clay-server 3.2.2-beta.3 → 3.3.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/git-cli.js +370 -0
- package/lib/git-session-attribution.js +219 -0
- package/lib/project-connection.js +1 -15
- package/lib/project-email.js +0 -61
- package/lib/project-http.js +104 -1
- package/lib/project-session-pair.js +129 -43
- package/lib/project-sessions.js +1 -11
- package/lib/project-user-message.js +2 -0
- package/lib/project-worker-proposal.js +317 -0
- package/lib/project.js +23 -8
- package/lib/public/app.js +14 -2
- package/lib/public/css/filebrowser.css +320 -0
- package/lib/public/css/git-panel.css +424 -0
- package/lib/public/css/input.css +0 -3
- package/lib/public/css/worker-proposal.css +127 -0
- package/lib/public/index.html +12 -15
- package/lib/public/modules/app-messages.js +11 -6
- package/lib/public/modules/context-sources.js +0 -90
- package/lib/public/modules/filebrowser.js +135 -21
- package/lib/public/modules/git-panel.js +456 -0
- package/lib/public/modules/input.js +8 -0
- package/lib/public/modules/markdown-slides.js +292 -0
- package/lib/public/modules/mate-sidebar.js +0 -7
- package/lib/public/modules/sidebar.js +42 -0
- package/lib/public/modules/tool-palette.js +1 -2
- package/lib/public/modules/worker-proposal.js +239 -0
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +47 -6
- package/lib/sdk-message-processor.js +26 -2
- package/lib/session-pair-mcp-server.js +2 -2
- package/lib/sessions.js +1 -1
- package/lib/ws-schema.js +3 -0
- package/lib/yoke/adapters/claude.js +43 -17
- package/lib/yoke/index.js +19 -0
- package/package.json +1 -1
package/lib/project-email.js
CHANGED
|
@@ -9,31 +9,6 @@ var smtp = require("./smtp");
|
|
|
9
9
|
var { CONFIG_DIR } = require("./config");
|
|
10
10
|
|
|
11
11
|
var AUDIT_LOG_PATH = path.join(CONFIG_DIR, "email-audit.jsonl");
|
|
12
|
-
var EMAIL_DEFAULTS_DIR = path.join(CONFIG_DIR, "email-defaults");
|
|
13
|
-
|
|
14
|
-
// --- Project-level email defaults ---
|
|
15
|
-
// Stores which email accounts should be auto-enabled for every new session.
|
|
16
|
-
|
|
17
|
-
function loadEmailDefaults(slug) {
|
|
18
|
-
try {
|
|
19
|
-
var filePath = path.join(EMAIL_DEFAULTS_DIR, slug + ".json");
|
|
20
|
-
var data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
21
|
-
return data.accounts || [];
|
|
22
|
-
} catch (e) {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function saveEmailDefaults(slug, accountIds) {
|
|
28
|
-
try {
|
|
29
|
-
fs.mkdirSync(EMAIL_DEFAULTS_DIR, { recursive: true });
|
|
30
|
-
var filePath = path.join(EMAIL_DEFAULTS_DIR, slug + ".json");
|
|
31
|
-
fs.writeFileSync(filePath, JSON.stringify({ accounts: accountIds }), "utf8");
|
|
32
|
-
} catch (e) {
|
|
33
|
-
console.error("[email] Failed to save defaults:", e.message);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
12
|
// --- Audit log (Server SMTP only) ---
|
|
38
13
|
|
|
39
14
|
function appendAuditLog(entry) {
|
|
@@ -185,7 +160,6 @@ function formatTimeAgo(date) {
|
|
|
185
160
|
function attachEmail(ctx) {
|
|
186
161
|
var slug = ctx.slug;
|
|
187
162
|
var send = ctx.send;
|
|
188
|
-
var sendTo = ctx.sendTo;
|
|
189
163
|
var clients = ctx.clients;
|
|
190
164
|
var loadContextSources = ctx.loadContextSources;
|
|
191
165
|
var getUserIdForWs = ctx.getUserIdForWs;
|
|
@@ -327,17 +301,6 @@ function attachEmail(ctx) {
|
|
|
327
301
|
}
|
|
328
302
|
}
|
|
329
303
|
}
|
|
330
|
-
// Fallback: if no session-level sources found, check project email defaults
|
|
331
|
-
if (result.length === 0) {
|
|
332
|
-
var defaults = loadEmailDefaults(slug);
|
|
333
|
-
for (var di = 0; di < defaults.length; di++) {
|
|
334
|
-
var dec = emailAccounts.getAccountDecrypted(userId, defaults[di]);
|
|
335
|
-
if (dec && !seen[dec.id]) {
|
|
336
|
-
seen[dec.id] = true;
|
|
337
|
-
result.push(dec);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
304
|
return result;
|
|
342
305
|
}
|
|
343
306
|
|
|
@@ -376,28 +339,6 @@ function attachEmail(ctx) {
|
|
|
376
339
|
};
|
|
377
340
|
}
|
|
378
341
|
|
|
379
|
-
function handleEmailMessage(ws, msg) {
|
|
380
|
-
if (msg.type === "email_defaults_get") {
|
|
381
|
-
var defaults = loadEmailDefaults(slug);
|
|
382
|
-
sendTo(ws, { type: "email_defaults", accounts: defaults });
|
|
383
|
-
return true;
|
|
384
|
-
}
|
|
385
|
-
if (msg.type === "email_defaults_save") {
|
|
386
|
-
var accountIds = msg.accounts || [];
|
|
387
|
-
saveEmailDefaults(slug, accountIds);
|
|
388
|
-
// Broadcast to all clients on this project
|
|
389
|
-
var _defMsg = JSON.stringify({ type: "email_defaults", accounts: accountIds });
|
|
390
|
-
for (var c of clients) { if (c.readyState === 1) c.send(_defMsg); }
|
|
391
|
-
return true;
|
|
392
|
-
}
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Get default email account IDs for new sessions
|
|
397
|
-
function getEmailDefaults() {
|
|
398
|
-
return loadEmailDefaults(slug);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
342
|
function destroy() {
|
|
402
343
|
if (pollTimer) {
|
|
403
344
|
clearInterval(pollTimer);
|
|
@@ -421,10 +362,8 @@ function attachEmail(ctx) {
|
|
|
421
362
|
}
|
|
422
363
|
|
|
423
364
|
return {
|
|
424
|
-
handleEmailMessage: handleEmailMessage,
|
|
425
365
|
getEmailContext: getEmailContext,
|
|
426
366
|
getCheckedEmailAccounts: getCheckedEmailAccounts,
|
|
427
|
-
getEmailDefaults: getEmailDefaults,
|
|
428
367
|
createMcpDeps: createMcpDeps,
|
|
429
368
|
hasEmailCapability: hasEmailCapability,
|
|
430
369
|
destroy: destroy,
|
package/lib/project-http.js
CHANGED
|
@@ -5,6 +5,7 @@ var crypto = require("crypto");
|
|
|
5
5
|
var { execFileSync, spawn } = require("child_process");
|
|
6
6
|
var { fsAsUser } = require("./os-users");
|
|
7
7
|
var usersModule = require("./users");
|
|
8
|
+
var gitCli = require("./git-cli");
|
|
8
9
|
|
|
9
10
|
var IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
|
|
10
11
|
var MIME_TYPES = {
|
|
@@ -47,6 +48,7 @@ function attachHTTP(ctx) {
|
|
|
47
48
|
var slug = ctx.slug;
|
|
48
49
|
var project = ctx.project;
|
|
49
50
|
var sm = ctx.sm;
|
|
51
|
+
var gitAttribution = ctx.gitAttribution;
|
|
50
52
|
var send = ctx.send;
|
|
51
53
|
var imagesDir = ctx.imagesDir;
|
|
52
54
|
var osUsers = ctx.osUsers;
|
|
@@ -643,7 +645,108 @@ function attachHTTP(ctx) {
|
|
|
643
645
|
return true;
|
|
644
646
|
}
|
|
645
647
|
|
|
646
|
-
// Git
|
|
648
|
+
// Git panel status and actions
|
|
649
|
+
var isGitPanelRequest = urlPath === "/api/git/status" ||
|
|
650
|
+
urlPath === "/api/git/action" || urlPath.indexOf("/api/git/file-diff?") === 0 ||
|
|
651
|
+
urlPath.indexOf("/api/git/session-diff?") === 0;
|
|
652
|
+
if (isGitPanelRequest && usersModule.isMultiUser()) {
|
|
653
|
+
var gitRequestUser = req._clayUser;
|
|
654
|
+
var gitPermissions = gitRequestUser ? usersModule.getEffectivePermissions(gitRequestUser, osUsers) : null;
|
|
655
|
+
if (!gitPermissions || !gitPermissions.fileBrowser) {
|
|
656
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
657
|
+
res.end('{"error":"Git access is not permitted"}');
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (req.method === "GET" && urlPath === "/api/git/status") {
|
|
663
|
+
var gitUserInfo = getOsUserInfoForReq(req);
|
|
664
|
+
try {
|
|
665
|
+
var gitStatus = gitCli.getStatus(cwd, gitUserInfo);
|
|
666
|
+
if (gitAttribution) {
|
|
667
|
+
var visibleGitSessions = sm.sessions;
|
|
668
|
+
if (usersModule.isMultiUser() && req._clayUser) {
|
|
669
|
+
visibleGitSessions = new Map();
|
|
670
|
+
sm.sessions.forEach(function (session, id) {
|
|
671
|
+
if (usersModule.canAccessSession(req._clayUser.id, session, { visibility: "public" })) {
|
|
672
|
+
visibleGitSessions.set(id, session);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
gitAttribution.decorateStatus(gitStatus, visibleGitSessions);
|
|
677
|
+
}
|
|
678
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
679
|
+
res.end(JSON.stringify(gitStatus));
|
|
680
|
+
} catch (e) {
|
|
681
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
682
|
+
res.end(JSON.stringify({ error: e.message || "Unable to read Git status" }));
|
|
683
|
+
}
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (req.method === "GET" && urlPath.indexOf("/api/git/file-diff?") === 0) {
|
|
688
|
+
var gitDiffParams = new URLSearchParams(urlPath.slice(urlPath.indexOf("?")));
|
|
689
|
+
var gitDiffPath = gitDiffParams.get("path");
|
|
690
|
+
if (!gitDiffPath) {
|
|
691
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
692
|
+
res.end('{"error":"Missing file path"}');
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
var gitDiffResult = gitCli.getFileDiff(cwd, gitDiffPath, getOsUserInfoForReq(req));
|
|
697
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
698
|
+
res.end(JSON.stringify(gitDiffResult));
|
|
699
|
+
} catch (e) {
|
|
700
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
701
|
+
res.end(JSON.stringify({ error: e.message || "Unable to read file diff" }));
|
|
702
|
+
}
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (req.method === "GET" && urlPath.indexOf("/api/git/session-diff?") === 0) {
|
|
707
|
+
var sessionDiffParams = new URLSearchParams(urlPath.slice(urlPath.indexOf("?")));
|
|
708
|
+
var sessionDiffKey = sessionDiffParams.get("session");
|
|
709
|
+
var sessionDiffPath = sessionDiffParams.get("path");
|
|
710
|
+
if (!gitAttribution || !sessionDiffKey || !sessionDiffPath) {
|
|
711
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
712
|
+
res.end('{"error":"Missing session comparison"}');
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
var requestedSessionVisible = false;
|
|
717
|
+
sm.sessions.forEach(function (session) {
|
|
718
|
+
var key = session.cliSessionId || ("local:" + session.localId);
|
|
719
|
+
if (key !== sessionDiffKey) return;
|
|
720
|
+
requestedSessionVisible = !usersModule.isMultiUser() ||
|
|
721
|
+
(req._clayUser && usersModule.canAccessSession(req._clayUser.id, session, { visibility: "public" }));
|
|
722
|
+
});
|
|
723
|
+
if (!requestedSessionVisible) throw new Error("Session comparison is unavailable");
|
|
724
|
+
var sessionDiff = gitAttribution.getSessionBaselineDiff(sessionDiffKey, sessionDiffPath, getOsUserInfoForReq(req));
|
|
725
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
726
|
+
res.end(JSON.stringify(sessionDiff));
|
|
727
|
+
} catch (e) {
|
|
728
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
729
|
+
res.end(JSON.stringify({ error: e.message || "Unable to compare session changes" }));
|
|
730
|
+
}
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (req.method === "POST" && urlPath === "/api/git/action") {
|
|
735
|
+
var gitActionUserInfo = getOsUserInfoForReq(req);
|
|
736
|
+
parseJsonBody(req).then(function (body) {
|
|
737
|
+
return gitCli.runAction(cwd, body, gitActionUserInfo);
|
|
738
|
+
}).then(function (output) {
|
|
739
|
+
var updatedStatus = gitCli.getStatus(cwd, gitActionUserInfo);
|
|
740
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
741
|
+
res.end(JSON.stringify({ ok: true, output: output, status: updatedStatus }));
|
|
742
|
+
}).catch(function (err) {
|
|
743
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
744
|
+
res.end(JSON.stringify({ error: err.userMessage || err.message || "Git action failed" }));
|
|
745
|
+
});
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Legacy dirty check used by the Ralph Loop flow
|
|
647
750
|
if (req.method === "GET" && urlPath === "/api/git-dirty") {
|
|
648
751
|
try {
|
|
649
752
|
var out = execFileSync("git", ["status", "--porcelain"], { cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
var pairMcp = require("./session-pair-mcp-server");
|
|
2
|
+
var { attachWorkerProposal } = require("./project-worker-proposal");
|
|
2
3
|
var yoke = require("./yoke");
|
|
3
4
|
|
|
4
5
|
var MAX_RESPONSE_CHARS = 30000;
|
|
@@ -57,6 +58,7 @@ function recentTurns(session, count) {
|
|
|
57
58
|
function attachSessionPair(ctx) {
|
|
58
59
|
var sm = ctx.sm;
|
|
59
60
|
var store = ctx.splitStore;
|
|
61
|
+
var workerProposal;
|
|
60
62
|
|
|
61
63
|
function groupAndPartner(caller) {
|
|
62
64
|
if (!caller) throw new Error("partner tools require a session-bound tool server");
|
|
@@ -91,6 +93,63 @@ function attachSessionPair(ctx) {
|
|
|
91
93
|
broadcastDelegation(group, caller, partner, false);
|
|
92
94
|
}
|
|
93
95
|
|
|
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 = "A Worker task delegated through send_to_partner has finished.\n\n" +
|
|
102
|
+
"Original task:\n" + token.message + "\n\n" +
|
|
103
|
+
(failure ? "Worker error:\n" + failure : "Worker result:\n" + (response || "(No text response was recorded.)")) +
|
|
104
|
+
"\n\nReview the result, verify it as needed, and continue the task.";
|
|
105
|
+
sm.sendAndRecord(caller, {
|
|
106
|
+
type: "user_message",
|
|
107
|
+
text: text,
|
|
108
|
+
_internal: true,
|
|
109
|
+
partnerResult: true,
|
|
110
|
+
partnerSessionId: partner.localId,
|
|
111
|
+
});
|
|
112
|
+
caller.lastActivity = Date.now();
|
|
113
|
+
var sdk = ctx.getSdk();
|
|
114
|
+
if (!sdk) {
|
|
115
|
+
sm.sendAndRecord(caller, { type: "error", text: "Could not resume the Driver: SDK bridge is not ready" });
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (!caller.isProcessing) {
|
|
119
|
+
caller.isProcessing = true;
|
|
120
|
+
caller.sentToolResults = {};
|
|
121
|
+
ctx.onProcessingChanged();
|
|
122
|
+
sm.sendToSession(caller, { type: "status", status: "processing" });
|
|
123
|
+
}
|
|
124
|
+
if (!sdk.pushMessage(caller, text)) {
|
|
125
|
+
caller._queryStartTs = Date.now();
|
|
126
|
+
Promise.resolve(sdk.startQuery(caller, text, undefined, ctx.getLinuxUserForSession(caller))).catch(function (err) {
|
|
127
|
+
caller.isProcessing = false;
|
|
128
|
+
sm.sendAndRecord(caller, { type: "error", text: err.message || String(err) });
|
|
129
|
+
ctx.onProcessingChanged();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
sm.broadcastSessionList();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function handleTurnDone(partner) {
|
|
137
|
+
var token = partner && partner._pairDelegation;
|
|
138
|
+
if (!token) return false;
|
|
139
|
+
var group = store.groupForMember(partner.localId);
|
|
140
|
+
if (!group || group.id !== token.groupId || group.members.indexOf(token.from) === -1) return false;
|
|
141
|
+
var caller = sm.sessions.get(token.from);
|
|
142
|
+
if (!caller || caller.ownerId !== partner.ownerId) return false;
|
|
143
|
+
if (group.pair && (group.pair.driverId !== caller.localId || group.pair.workerId !== partner.localId)) {
|
|
144
|
+
finishDelegation(group, caller, partner, token);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
token.response = responseText(partner.history || [], token.startIndex);
|
|
148
|
+
token.failure = errorSince(partner.history || [], token.startIndex);
|
|
149
|
+
finishDelegation(group, caller, partner, token);
|
|
150
|
+
return resumeDriverWithResult(caller, partner, token);
|
|
151
|
+
}
|
|
152
|
+
|
|
94
153
|
function waitForPartner(group, caller, partner, token, timeoutSeconds) {
|
|
95
154
|
return new Promise(function (resolve, reject) {
|
|
96
155
|
var deadline = Date.now() + timeoutSeconds * 1000;
|
|
@@ -115,8 +174,9 @@ function attachSessionPair(ctx) {
|
|
|
115
174
|
}
|
|
116
175
|
if (Date.now() >= deadline) {
|
|
117
176
|
clearInterval(timer);
|
|
177
|
+
token.detached = true;
|
|
118
178
|
monitorPartner(group, caller, partner, token);
|
|
119
|
-
resolve({ status: "running", response: responseText(partner.history || [], token.startIndex), hint: "Use read_partner
|
|
179
|
+
resolve({ status: "running", response: responseText(partner.history || [], token.startIndex), hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
|
|
120
180
|
}
|
|
121
181
|
}, 500);
|
|
122
182
|
});
|
|
@@ -127,7 +187,8 @@ function attachSessionPair(ctx) {
|
|
|
127
187
|
var currentGroup = store.groupForMember(caller.localId);
|
|
128
188
|
if (!currentGroup || currentGroup.id !== group.id || (!partner.isProcessing && !partner._queryStarting)) {
|
|
129
189
|
clearInterval(timer);
|
|
130
|
-
|
|
190
|
+
if (currentGroup && currentGroup.id === group.id) handleTurnDone(partner);
|
|
191
|
+
else finishDelegation(group, caller, partner, token);
|
|
131
192
|
}
|
|
132
193
|
}, 500);
|
|
133
194
|
}
|
|
@@ -143,7 +204,7 @@ function attachSessionPair(ctx) {
|
|
|
143
204
|
timeout = Math.max(1, Math.min(900, timeout));
|
|
144
205
|
var partner = resolved.partner;
|
|
145
206
|
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 };
|
|
207
|
+
var token = { from: caller.localId, groupId: resolved.group.id, startIndex: partner.history.length, message: message };
|
|
147
208
|
partner._pairDelegation = token;
|
|
148
209
|
partner._delegatedBy = caller.localId;
|
|
149
210
|
sm.sendAndRecord(partner, {
|
|
@@ -173,8 +234,9 @@ function attachSessionPair(ctx) {
|
|
|
173
234
|
}
|
|
174
235
|
sm.broadcastSessionList();
|
|
175
236
|
if (!wait) {
|
|
237
|
+
token.detached = true;
|
|
176
238
|
monitorPartner(resolved.group, caller, partner, token);
|
|
177
|
-
return toolResult({ status: "running", partnerId: partner.localId, hint: "Use read_partner
|
|
239
|
+
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." });
|
|
178
240
|
}
|
|
179
241
|
return toolResult(await waitForPartner(resolved.group, caller, partner, token, timeout));
|
|
180
242
|
} catch (e) {
|
|
@@ -223,10 +285,11 @@ function attachSessionPair(ctx) {
|
|
|
223
285
|
// pair worker at query start never sees the tools.
|
|
224
286
|
var group = store.groupForMember(boundSession.localId);
|
|
225
287
|
if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
|
|
226
|
-
|
|
288
|
+
var tools = pairMcp.getToolDefs({
|
|
227
289
|
send: function (args) { return sendToPartner(args, boundSession); },
|
|
228
290
|
read: function (args) { return readPartner(args, boundSession); },
|
|
229
291
|
});
|
|
292
|
+
return tools.concat(workerProposal.getToolDefs(boundSession));
|
|
230
293
|
}
|
|
231
294
|
|
|
232
295
|
function validateVendor(vendor) {
|
|
@@ -235,49 +298,69 @@ function attachSessionPair(ctx) {
|
|
|
235
298
|
return vendor;
|
|
236
299
|
}
|
|
237
300
|
|
|
301
|
+
function createPairRecord(ws, msg) {
|
|
302
|
+
if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
|
|
303
|
+
var driverSpec = msg.driver || {};
|
|
304
|
+
var workerSpec = msg.worker || {};
|
|
305
|
+
var workerVendor = validateVendor(workerSpec.vendor || sm.lastVendor || "codex");
|
|
306
|
+
var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
|
|
307
|
+
var driver;
|
|
308
|
+
var groupName;
|
|
309
|
+
if (Number.isInteger(driverSpec.sessionId)) {
|
|
310
|
+
// "Add Worker" on an existing session: the current session becomes
|
|
311
|
+
// the Driver with its full conversation context intact.
|
|
312
|
+
driver = sm.sessions.get(driverSpec.sessionId);
|
|
313
|
+
if (!driver) throw new Error("driver session not found");
|
|
314
|
+
if ((driver.ownerId || null) !== ownerId) throw new Error("driver session access denied");
|
|
315
|
+
if (store.groupForMember(driver.localId)) throw new Error("this session is already in a split group");
|
|
316
|
+
groupName = undefined; // auto name from member titles
|
|
317
|
+
} else {
|
|
318
|
+
var driverVendor = validateVendor(driverSpec.vendor || "claude");
|
|
319
|
+
var driverEffort = yoke.clampEffort(driverVendor, driverSpec.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[driverVendor]) || sm.currentEffort || "medium") || null;
|
|
320
|
+
driver = sm.createSessionRaw({ ownerId: ownerId, vendor: driverVendor, model: driverSpec.model || null, effort: driverEffort });
|
|
321
|
+
driver.title = "Driver · " + ((yoke.getVendorInfo(driverVendor) || {}).displayName || driverVendor);
|
|
322
|
+
if (driverSpec.effort) driver.loopSettings = { effort: driverSpec.effort };
|
|
323
|
+
groupName = "Agent pair";
|
|
324
|
+
}
|
|
325
|
+
var workerEffort = yoke.clampEffort(workerVendor, workerSpec.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[workerVendor]) || sm.currentEffort || "medium") || null;
|
|
326
|
+
var worker = sm.createSessionRaw({ ownerId: ownerId, vendor: workerVendor, model: workerSpec.model || null, effort: workerEffort });
|
|
327
|
+
worker.title = "Worker · " + ((yoke.getVendorInfo(workerVendor) || {}).displayName || workerVendor);
|
|
328
|
+
if (workerSpec.effort) worker.loopSettings = { effort: workerSpec.effort };
|
|
329
|
+
var result = store.create(ws, {
|
|
330
|
+
members: [driver.localId, worker.localId],
|
|
331
|
+
pair: { driverId: driver.localId, workerId: worker.localId },
|
|
332
|
+
name: groupName,
|
|
333
|
+
});
|
|
334
|
+
if (!result.ok) throw new Error(result.error);
|
|
335
|
+
sm.broadcastSessionList();
|
|
336
|
+
return { driver: driver, worker: worker, group: result.group };
|
|
337
|
+
}
|
|
338
|
+
|
|
238
339
|
function createPair(ws, msg) {
|
|
239
340
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
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 });
|
|
341
|
+
var created = createPairRecord(ws, msg);
|
|
342
|
+
ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
|
|
275
343
|
} catch (e) {
|
|
276
344
|
ctx.sendTo(ws, { type: "pair_session_created", ok: false, error: e.message || String(e) });
|
|
277
345
|
}
|
|
278
346
|
}
|
|
279
347
|
|
|
348
|
+
workerProposal = attachWorkerProposal({
|
|
349
|
+
sm: sm,
|
|
350
|
+
isMate: ctx.isMate,
|
|
351
|
+
splitStore: store,
|
|
352
|
+
getSdk: ctx.getSdk,
|
|
353
|
+
sendTo: ctx.sendTo,
|
|
354
|
+
usersModule: ctx.usersModule,
|
|
355
|
+
getLinuxUserForSession: ctx.getLinuxUserForSession,
|
|
356
|
+
onProcessingChanged: ctx.onProcessingChanged,
|
|
357
|
+
adapters: ctx.adapters,
|
|
358
|
+
createPairRecord: createPairRecord,
|
|
359
|
+
sendToPartner: sendToPartner,
|
|
360
|
+
});
|
|
361
|
+
|
|
280
362
|
function handleMessage(ws, msg) {
|
|
363
|
+
if (workerProposal.handleMessage(ws, msg)) return true;
|
|
281
364
|
if (msg.type === "pair_session_options") {
|
|
282
365
|
if (ctx.isMate) return false;
|
|
283
366
|
ctx.sendTo(ws, {
|
|
@@ -298,11 +381,14 @@ function attachSessionPair(ctx) {
|
|
|
298
381
|
|
|
299
382
|
function getSystemPrompt(session) {
|
|
300
383
|
var group = store.groupForMember(session.localId);
|
|
301
|
-
|
|
302
|
-
|
|
384
|
+
var pairPrompt = "";
|
|
385
|
+
if (group && group.pair && group.pair.driverId === session.localId) {
|
|
386
|
+
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. 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.";
|
|
387
|
+
}
|
|
388
|
+
return [pairPrompt, workerProposal.getSystemPrompt(session)].filter(Boolean).join("\n\n");
|
|
303
389
|
}
|
|
304
390
|
|
|
305
|
-
return { getToolDefs: getToolDefs, handleMessage: handleMessage, getSystemPrompt: getSystemPrompt };
|
|
391
|
+
return { getToolDefs: getToolDefs, handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: getSystemPrompt };
|
|
306
392
|
}
|
|
307
393
|
|
|
308
394
|
module.exports = {
|
package/lib/project-sessions.js
CHANGED
|
@@ -104,7 +104,6 @@ function attachSessions(ctx) {
|
|
|
104
104
|
var getLatestVersion = ctx.getLatestVersion;
|
|
105
105
|
var setLatestVersion = ctx.setLatestVersion;
|
|
106
106
|
var loadContextSources = ctx.loadContextSources;
|
|
107
|
-
var saveContextSources = ctx.saveContextSources;
|
|
108
107
|
|
|
109
108
|
// Resolve the active user's Claude open-mode preference ('gui' or 'tui').
|
|
110
109
|
// Multi-user mode reads per-user storage; single-user mode falls back to
|
|
@@ -516,7 +515,7 @@ function attachSessions(ctx) {
|
|
|
516
515
|
// Reuse an existing blank GUI session instead of stacking another
|
|
517
516
|
// one. A vendor-less blank is acceptable: nothing has happened in it,
|
|
518
517
|
// so stamping the requested vendor is equivalent to a fresh create.
|
|
519
|
-
var reusable = sm.findReusableBlankSession({
|
|
518
|
+
var reusable = msg.forceNew === true ? null : sm.findReusableBlankSession({
|
|
520
519
|
vendor: sessionOpts.vendor || null,
|
|
521
520
|
ownerId: sessionOpts.ownerId || null,
|
|
522
521
|
});
|
|
@@ -542,15 +541,6 @@ function attachSessions(ctx) {
|
|
|
542
541
|
}
|
|
543
542
|
send({ type: "last_vendor", vendor: msg.vendor });
|
|
544
543
|
}
|
|
545
|
-
// Apply project-level email defaults to new session
|
|
546
|
-
if (typeof ctx._email === "object" && ctx._email.getEmailDefaults) {
|
|
547
|
-
var emailDefaults = ctx._email.getEmailDefaults();
|
|
548
|
-
if (emailDefaults.length > 0) {
|
|
549
|
-
var defaultSources = emailDefaults.map(function (id) { return "email:" + id; });
|
|
550
|
-
saveContextSources(slug, newSess.localId, defaultSources);
|
|
551
|
-
sendTo(ws, { type: "context_sources_state", active: defaultSources });
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
544
|
var nsPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
555
545
|
if (!ws._clayPane) userPresence.setPresence(slug, nsPresKey, newSess.localId, null);
|
|
556
546
|
if (usersModule.isMultiUser() && !ws._clayPane) {
|
|
@@ -57,6 +57,7 @@ function attachUserMessage(ctx) {
|
|
|
57
57
|
var imagesDir = ctx.imagesDir;
|
|
58
58
|
|
|
59
59
|
var onProcessingChanged = ctx.onProcessingChanged;
|
|
60
|
+
var gitAttribution = ctx.gitAttribution;
|
|
60
61
|
|
|
61
62
|
var _loop = ctx._loop;
|
|
62
63
|
var browserState = ctx.browserState;
|
|
@@ -293,6 +294,7 @@ function attachUserMessage(ctx) {
|
|
|
293
294
|
|
|
294
295
|
var session = getSessionForWs(ws);
|
|
295
296
|
if (!session) return true;
|
|
297
|
+
if (gitAttribution) gitAttribution.beginTurn(session);
|
|
296
298
|
|
|
297
299
|
// Bind vendor to session on first message (if not already set)
|
|
298
300
|
if (!session.vendor && msg.vendor) {
|