clay-server 4.0.1-beta.1 → 4.1.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-project-access.js +118 -0
- package/lib/daemon-projects.js +19 -5
- package/lib/daemon.js +19 -18
- package/lib/project-logs-access-scope.js +94 -0
- package/lib/project-logs-query.js +3 -0
- package/lib/project-logs-service.js +86 -84
- package/lib/project-logs-store.js +5 -3
- package/lib/project-pair-lifecycle.js +140 -102
- package/lib/project-pair-message.js +50 -0
- package/lib/project-pair-replacement-state.js +72 -0
- package/lib/project-pair-task-control.js +264 -0
- package/lib/project-pair-usage.js +76 -0
- package/lib/project-session-pair.js +115 -116
- package/lib/project-sessions.js +25 -1
- package/lib/project-worker-proposal.js +69 -58
- package/lib/project.js +21 -4
- package/lib/public/css/icon-strip.css +40 -0
- package/lib/public/modules/app-messages.js +9 -0
- package/lib/public/modules/project-access.js +141 -0
- package/lib/public/modules/project-grouping.js +22 -0
- package/lib/public/modules/sidebar-projects.js +14 -171
- package/lib/public/modules/worker-proposal.js +4 -0
- package/lib/sdk-bridge.js +20 -0
- package/lib/server-admin.js +3 -3
- package/lib/server-global-ws.js +10 -0
- package/lib/server-mates.js +4 -3
- package/lib/server-palette.js +17 -14
- package/lib/server.js +184 -102
- package/lib/session-pair-factory.js +3 -0
- package/lib/session-pair-history.js +40 -0
- package/lib/session-pair-mcp-server.js +19 -2
- package/lib/session-pair-operation.js +11 -0
- package/lib/session-pair-prompts.js +24 -2
- package/lib/session-pair-turn-control.js +8 -3
- package/lib/session-provenance.js +1 -0
- package/lib/session-spawn-mcp-server.js +2 -1
- package/lib/sessions.js +2 -0
- package/lib/worker-proposal-control.js +69 -0
- package/lib/worker-proposal-decision.js +7 -0
- package/lib/worker-runtime-catalog.js +52 -0
- package/lib/workspace-query-access.js +3 -2
- package/lib/workspace-query-service.js +1 -1
- package/lib/worktree.js +8 -4
- package/lib/ws-schema.js +1 -0
- package/lib/yoke/adapters/codex.js +25 -7
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -202,6 +202,23 @@ function createServer(opts) {
|
|
|
202
202
|
var isRequestAuthed = auth.isRequestAuthed;
|
|
203
203
|
var parseCookies = auth.parseCookies;
|
|
204
204
|
|
|
205
|
+
function canUserAccessSlug(userId, slug) {
|
|
206
|
+
if (!userId || !slug) return false;
|
|
207
|
+
var project = projects && projects.get(slug);
|
|
208
|
+
var status = project && project.getStatus ? project.getStatus() : null;
|
|
209
|
+
if (status && status.isMate === true) {
|
|
210
|
+
return users.canAccessProject(userId, {
|
|
211
|
+
visibility: "private",
|
|
212
|
+
ownerId: status.projectOwnerId || null,
|
|
213
|
+
allowedUsers: [],
|
|
214
|
+
}) === true;
|
|
215
|
+
}
|
|
216
|
+
if (!onGetProjectAccess) return false;
|
|
217
|
+
var access = onGetProjectAccess(slug);
|
|
218
|
+
if (!access || access.error) return false;
|
|
219
|
+
return users.canAccessProject(userId, access) === true;
|
|
220
|
+
}
|
|
221
|
+
|
|
205
222
|
var realVersion = require("../package.json").version;
|
|
206
223
|
var currentVersion = debug ? "0.0.9" : realVersion;
|
|
207
224
|
|
|
@@ -339,6 +356,13 @@ function createServer(opts) {
|
|
|
339
356
|
if (!access || access.error) return false;
|
|
340
357
|
return users.canAccessProject(userId, access) === true;
|
|
341
358
|
},
|
|
359
|
+
hasFullProjectAccess: function (userId, status) {
|
|
360
|
+
if (!status || !status.slug || !onGetProjectAccess) return false;
|
|
361
|
+
var access = onGetProjectAccess(status.slug);
|
|
362
|
+
if (!access || access.error) return false;
|
|
363
|
+
var governing = access.isWorktree && access.parentAccess ? access.parentAccess : access;
|
|
364
|
+
return users.canAccessProject(userId, governing) === true;
|
|
365
|
+
},
|
|
342
366
|
resolveMate: function (userId, mateId) {
|
|
343
367
|
return mates.getMate(mates.buildMateCtx(users.isMultiUser() ? userId : null), mateId);
|
|
344
368
|
},
|
|
@@ -403,6 +427,7 @@ function createServer(opts) {
|
|
|
403
427
|
projects: projects,
|
|
404
428
|
getMultiUserFromReq: getMultiUserFromReq,
|
|
405
429
|
onGetProjectAccess: onGetProjectAccess,
|
|
430
|
+
canUserAccessSlug: canUserAccessSlug,
|
|
406
431
|
});
|
|
407
432
|
|
|
408
433
|
// --- Push module (global) ---
|
|
@@ -662,7 +687,11 @@ function createServer(opts) {
|
|
|
662
687
|
if (isRequestAuthed(req)) {
|
|
663
688
|
var mem = process.memoryUsage();
|
|
664
689
|
var activeSessions = 0;
|
|
665
|
-
|
|
690
|
+
var healthProjects = 0;
|
|
691
|
+
var healthUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
|
|
692
|
+
projects.forEach(function (ctx, slug) {
|
|
693
|
+
if (users.isMultiUser() && (!healthUser || !canUserAccessSlug(healthUser.id, slug))) return;
|
|
694
|
+
healthProjects++;
|
|
666
695
|
if (ctx && ctx.clients) {
|
|
667
696
|
activeSessions += ctx.clients.size || 0;
|
|
668
697
|
}
|
|
@@ -671,7 +700,7 @@ function createServer(opts) {
|
|
|
671
700
|
health.version = pkg.version;
|
|
672
701
|
health.node = process.version;
|
|
673
702
|
health.sessions = activeSessions;
|
|
674
|
-
health.projects =
|
|
703
|
+
health.projects = healthProjects;
|
|
675
704
|
health.memory = {
|
|
676
705
|
rss: mem.rss,
|
|
677
706
|
heapUsed: mem.heapUsed,
|
|
@@ -850,7 +879,9 @@ function createServer(opts) {
|
|
|
850
879
|
return;
|
|
851
880
|
}
|
|
852
881
|
var projectList = [];
|
|
882
|
+
var infoUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
|
|
853
883
|
projects.forEach(function (ctx, slug) {
|
|
884
|
+
if (users.isMultiUser() && (!infoUser || !canUserAccessSlug(infoUser.id, slug))) return;
|
|
854
885
|
projectList.push({ slug: slug, project: ctx.project });
|
|
855
886
|
});
|
|
856
887
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -912,15 +943,12 @@ function createServer(opts) {
|
|
|
912
943
|
res.setHeader("Set-Cookie", "clay_last_project=" + slug + "; Path=/; SameSite=Strict; Max-Age=31536000" + (tlsOptions ? "; Secure" : ""));
|
|
913
944
|
|
|
914
945
|
// Multi-user: check project access for HTTP requests
|
|
915
|
-
if (users.isMultiUser()
|
|
946
|
+
if (users.isMultiUser()) {
|
|
916
947
|
var httpUser = getMultiUserFromReq(req);
|
|
917
|
-
if (httpUser) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
res.end();
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
948
|
+
if (!httpUser || !canUserAccessSlug(httpUser.id, slug)) {
|
|
949
|
+
res.writeHead(302, { "Location": "/" });
|
|
950
|
+
res.end();
|
|
951
|
+
return;
|
|
924
952
|
}
|
|
925
953
|
}
|
|
926
954
|
|
|
@@ -1029,6 +1057,9 @@ function createServer(opts) {
|
|
|
1029
1057
|
onAppMessage: function (ws, msg) {
|
|
1030
1058
|
return handleDmMessage(ws, msg);
|
|
1031
1059
|
},
|
|
1060
|
+
onConnection: function (ws) {
|
|
1061
|
+
sendProjectsUpdatedToClient(ws);
|
|
1062
|
+
},
|
|
1032
1063
|
});
|
|
1033
1064
|
|
|
1034
1065
|
server.on("upgrade", function (req, socket, head) {
|
|
@@ -1106,19 +1137,19 @@ function createServer(opts) {
|
|
|
1106
1137
|
if (users.isMultiUser()) {
|
|
1107
1138
|
wsUser = getMultiUserFromReq(req);
|
|
1108
1139
|
// Check project access for multi-user mode
|
|
1109
|
-
if (wsUser
|
|
1110
|
-
|
|
1111
|
-
var accessSlug = (wsSlug.indexOf("--") !== -1) ? wsSlug.split("--")[0] : wsSlug;
|
|
1112
|
-
var projectAccess = onGetProjectAccess(accessSlug);
|
|
1140
|
+
if (wsUser) {
|
|
1141
|
+
var projectAccess = onGetProjectAccess ? onGetProjectAccess(wsSlug) : null;
|
|
1113
1142
|
if (debug) console.log("[server] WS access check:", wsSlug, "user:", wsUser.id, "role:", wsUser.role, "visibility:", projectAccess && projectAccess.visibility, "ownerId:", projectAccess && projectAccess.ownerId, "allowed:", projectAccess && projectAccess.allowedUsers);
|
|
1114
|
-
if (
|
|
1115
|
-
if (
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1143
|
+
if (!canUserAccessSlug(wsUser.id, wsSlug)) {
|
|
1144
|
+
if (debug) console.log("[server] WS rejected: access denied for", wsUser.id, "on", wsSlug);
|
|
1145
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
1146
|
+
socket.destroy();
|
|
1147
|
+
return;
|
|
1121
1148
|
}
|
|
1149
|
+
} else {
|
|
1150
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
1151
|
+
socket.destroy();
|
|
1152
|
+
return;
|
|
1122
1153
|
}
|
|
1123
1154
|
}
|
|
1124
1155
|
|
|
@@ -1200,13 +1231,9 @@ function createServer(opts) {
|
|
|
1200
1231
|
var filtered = allProjectsList;
|
|
1201
1232
|
if (users.isMultiUser() && onGetProjectAccess) {
|
|
1202
1233
|
var wsUser = ws._clayUser;
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (!access || access.error) return true;
|
|
1207
|
-
return users.canAccessProject(wsUser.id, access);
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1234
|
+
filtered = wsUser ? allProjectsList.filter(function (p) {
|
|
1235
|
+
return canUserAccessSlug(wsUser.id, p.slug);
|
|
1236
|
+
}) : [];
|
|
1210
1237
|
}
|
|
1211
1238
|
// Attach per-project unread counts for this client
|
|
1212
1239
|
var unreadMap = getCrossProjectUnread(ws);
|
|
@@ -1306,20 +1333,12 @@ function createServer(opts) {
|
|
|
1306
1333
|
authToken: pinHash || null,
|
|
1307
1334
|
getProjectCount: function () { return projects.size; },
|
|
1308
1335
|
getProjectList: function (userId) {
|
|
1309
|
-
|
|
1310
|
-
projects.forEach(function (ctx, s) {
|
|
1311
|
-
var status = ctx.getStatus();
|
|
1312
|
-
if (userId && users.isMultiUser() && onGetProjectAccess) {
|
|
1313
|
-
var access = onGetProjectAccess(s);
|
|
1314
|
-
if (access && !access.error && !users.canAccessProject(userId, access)) return;
|
|
1315
|
-
}
|
|
1316
|
-
list.push(status);
|
|
1317
|
-
});
|
|
1318
|
-
return list;
|
|
1336
|
+
return getVisibleProjects(userId);
|
|
1319
1337
|
},
|
|
1320
|
-
getHubSchedules: function () {
|
|
1338
|
+
getHubSchedules: function (userId) {
|
|
1321
1339
|
var allSchedules = [];
|
|
1322
1340
|
projects.forEach(function (ctx, s) {
|
|
1341
|
+
if (users.isMultiUser() && (!userId || !canUserAccessSlug(userId, s))) return;
|
|
1323
1342
|
var status = ctx.getStatus();
|
|
1324
1343
|
var recs = ctx.getSchedules();
|
|
1325
1344
|
for (var i = 0; i < recs.length; i++) {
|
|
@@ -1379,6 +1398,7 @@ function createServer(opts) {
|
|
|
1379
1398
|
if (!ctx) return 0;
|
|
1380
1399
|
return ctx.getSchedules().length;
|
|
1381
1400
|
},
|
|
1401
|
+
canAccessProjectSlug: canUserAccessSlug,
|
|
1382
1402
|
onPresenceChange: broadcastPresenceChange,
|
|
1383
1403
|
onProcessingChanged: broadcastProcessingChange,
|
|
1384
1404
|
onSessionDone: function () { onSessionDone(slug); },
|
|
@@ -1592,27 +1612,81 @@ function createServer(opts) {
|
|
|
1592
1612
|
function getServerUsers() {
|
|
1593
1613
|
var seen = {};
|
|
1594
1614
|
var list = [];
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
avatarCustom: p.avatarCustom || "",
|
|
1610
|
-
});
|
|
1615
|
+
forEachAppClient(function (ws) {
|
|
1616
|
+
if (!ws._clayUser) return;
|
|
1617
|
+
var u = ws._clayUser;
|
|
1618
|
+
if (seen[u.id]) return;
|
|
1619
|
+
seen[u.id] = true;
|
|
1620
|
+
var p = u.profile || {};
|
|
1621
|
+
list.push({
|
|
1622
|
+
id: u.id,
|
|
1623
|
+
displayName: p.name || u.displayName || u.username,
|
|
1624
|
+
username: u.username,
|
|
1625
|
+
avatarStyle: p.avatarStyle || "imprint",
|
|
1626
|
+
avatarSeed: p.avatarSeed || u.username,
|
|
1627
|
+
avatarColor: p.avatarColor || "#5857fc",
|
|
1628
|
+
avatarCustom: p.avatarCustom || "",
|
|
1611
1629
|
});
|
|
1612
1630
|
});
|
|
1613
1631
|
return list;
|
|
1614
1632
|
}
|
|
1615
1633
|
|
|
1634
|
+
function forEachAppClient(visitor) {
|
|
1635
|
+
if (globalWs && typeof globalWs.forEachClient === "function") {
|
|
1636
|
+
globalWs.forEachClient(visitor);
|
|
1637
|
+
}
|
|
1638
|
+
projects.forEach(function (ctx) {
|
|
1639
|
+
ctx.forEachClient(visitor);
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function getVisibleProjects(userId) {
|
|
1644
|
+
var visible = [];
|
|
1645
|
+
projects.forEach(function (ctx, slug) {
|
|
1646
|
+
if (users.isMultiUser() && (!userId || !canUserAccessSlug(userId, slug))) return;
|
|
1647
|
+
visible.push(ctx.getStatus());
|
|
1648
|
+
});
|
|
1649
|
+
return visible;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function sendProjectsUpdatedToClient(ws, serverUsers, allUsers) {
|
|
1653
|
+
if (!ws || ws.readyState !== 1) return;
|
|
1654
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1655
|
+
var visibleProjects = getVisibleProjects(userId);
|
|
1656
|
+
var message = {
|
|
1657
|
+
type: "projects_updated",
|
|
1658
|
+
projects: visibleProjects,
|
|
1659
|
+
projectCount: visibleProjects.length,
|
|
1660
|
+
removedProjects: getRemovedProjects(userId),
|
|
1661
|
+
};
|
|
1662
|
+
if (users.isMultiUser()) {
|
|
1663
|
+
message.serverUsers = serverUsers || getServerUsers();
|
|
1664
|
+
message.allUsers = allUsers || users.getAllUsers().map(function (user) {
|
|
1665
|
+
var profile = user.profile || {};
|
|
1666
|
+
return {
|
|
1667
|
+
id: user.id,
|
|
1668
|
+
displayName: profile.name || user.displayName || user.username,
|
|
1669
|
+
username: user.username,
|
|
1670
|
+
role: user.role,
|
|
1671
|
+
avatarStyle: profile.avatarStyle || "imprint",
|
|
1672
|
+
avatarSeed: profile.avatarSeed || user.username,
|
|
1673
|
+
avatarColor: profile.avatarColor || "#5857fc",
|
|
1674
|
+
avatarCustom: profile.avatarCustom || "",
|
|
1675
|
+
};
|
|
1676
|
+
});
|
|
1677
|
+
message.dmFavorites = userId ? users.getDmFavorites(userId) : [];
|
|
1678
|
+
message.dmConversations = [];
|
|
1679
|
+
if (userId) {
|
|
1680
|
+
var hidden = users.getDmHidden(userId);
|
|
1681
|
+
var dmList = dm.getDmList(userId);
|
|
1682
|
+
for (var i = 0; i < dmList.length; i++) {
|
|
1683
|
+
if (hidden.indexOf(dmList[i].otherUserId) === -1) message.dmConversations.push(dmList[i].otherUserId);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
ws.send(JSON.stringify(message));
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1616
1690
|
// Debounced broadcast of projects_updated when presence changes
|
|
1617
1691
|
// Sends per-user filtered project lists + server-wide user list
|
|
1618
1692
|
var presenceTimer = null;
|
|
@@ -1621,12 +1695,7 @@ function createServer(opts) {
|
|
|
1621
1695
|
presenceTimer = setTimeout(function () {
|
|
1622
1696
|
presenceTimer = null;
|
|
1623
1697
|
if (!users.isMultiUser()) {
|
|
1624
|
-
|
|
1625
|
-
type: "projects_updated",
|
|
1626
|
-
projects: getProjects(),
|
|
1627
|
-
projectCount: projects.size,
|
|
1628
|
-
removedProjects: getRemovedProjects(),
|
|
1629
|
-
});
|
|
1698
|
+
forEachAppClient(function (ws) { sendProjectsUpdatedToClient(ws); });
|
|
1630
1699
|
return;
|
|
1631
1700
|
}
|
|
1632
1701
|
var serverUsers = getServerUsers();
|
|
@@ -1643,61 +1712,73 @@ function createServer(opts) {
|
|
|
1643
1712
|
avatarCustom: p.avatarCustom || "",
|
|
1644
1713
|
};
|
|
1645
1714
|
});
|
|
1646
|
-
// Build per-user filtered lists, send individually
|
|
1715
|
+
// Build per-user filtered lists, send individually to both project and
|
|
1716
|
+
// slug-less Home sockets. A Home fallback must replace, never retain,
|
|
1717
|
+
// the project cache from its previous authenticated project socket.
|
|
1647
1718
|
var sentUsers = {};
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
// Per-user DM data
|
|
1667
|
-
var userDmFavorites = userId ? users.getDmFavorites(userId) : [];
|
|
1668
|
-
var userDmHidden = userId ? users.getDmHidden(userId) : [];
|
|
1669
|
-
var userDmConversations = [];
|
|
1670
|
-
if (userId) {
|
|
1671
|
-
var dmList = dm.getDmList(userId);
|
|
1672
|
-
for (var di = 0; di < dmList.length; di++) {
|
|
1673
|
-
if (userDmHidden.indexOf(dmList[di].otherUserId) === -1) {
|
|
1674
|
-
userDmConversations.push(dmList[di].otherUserId);
|
|
1675
|
-
}
|
|
1719
|
+
forEachAppClient(function (ws) {
|
|
1720
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1721
|
+
var key = userId || "__anon__";
|
|
1722
|
+
if (sentUsers[key]) {
|
|
1723
|
+
// Already computed for this user, just send the cached msg
|
|
1724
|
+
if (ws.readyState === 1) ws.send(sentUsers[key]);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
var filteredProjects = getVisibleProjects(userId);
|
|
1728
|
+
// Per-user DM data
|
|
1729
|
+
var userDmFavorites = userId ? users.getDmFavorites(userId) : [];
|
|
1730
|
+
var userDmHidden = userId ? users.getDmHidden(userId) : [];
|
|
1731
|
+
var userDmConversations = [];
|
|
1732
|
+
if (userId) {
|
|
1733
|
+
var dmList = dm.getDmList(userId);
|
|
1734
|
+
for (var di = 0; di < dmList.length; di++) {
|
|
1735
|
+
if (userDmHidden.indexOf(dmList[di].otherUserId) === -1) {
|
|
1736
|
+
userDmConversations.push(dmList[di].otherUserId);
|
|
1676
1737
|
}
|
|
1677
1738
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
sentUsers[key] = msgStr;
|
|
1689
|
-
ws.send(msgStr);
|
|
1739
|
+
}
|
|
1740
|
+
var msgStr = JSON.stringify({
|
|
1741
|
+
type: "projects_updated",
|
|
1742
|
+
projects: filteredProjects,
|
|
1743
|
+
projectCount: filteredProjects.length,
|
|
1744
|
+
serverUsers: serverUsers,
|
|
1745
|
+
allUsers: allUsers,
|
|
1746
|
+
dmFavorites: userDmFavorites,
|
|
1747
|
+
dmConversations: userDmConversations,
|
|
1748
|
+
removedProjects: getRemovedProjects(userId),
|
|
1690
1749
|
});
|
|
1750
|
+
sentUsers[key] = msgStr;
|
|
1751
|
+
if (ws.readyState === 1) ws.send(msgStr);
|
|
1691
1752
|
});
|
|
1692
1753
|
}, 300);
|
|
1693
1754
|
}
|
|
1694
1755
|
|
|
1695
1756
|
function broadcastAll(msg) {
|
|
1757
|
+
if (msg && msg.type === "projects_updated") {
|
|
1758
|
+
broadcastPresenceChange();
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1696
1761
|
projects.forEach(function (ctx) {
|
|
1697
1762
|
ctx.send(msg);
|
|
1698
1763
|
});
|
|
1699
1764
|
}
|
|
1700
1765
|
|
|
1766
|
+
function refreshProjectAccess() {
|
|
1767
|
+
if (!users.isMultiUser()) return;
|
|
1768
|
+
projects.forEach(function (ctx, slug) {
|
|
1769
|
+
if (!ctx || typeof ctx.forEachClient !== "function") return;
|
|
1770
|
+
ctx.forEachClient(function (ws) {
|
|
1771
|
+
var userId = ws._clayUser && ws._clayUser.id;
|
|
1772
|
+
if (userId && canUserAccessSlug(userId, slug)) return;
|
|
1773
|
+
try {
|
|
1774
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: "access_revoked", slug: slug }));
|
|
1775
|
+
ws.close(1008, "Project access revoked");
|
|
1776
|
+
} catch (e) {}
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
broadcastPresenceChange();
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1701
1782
|
// Send a message to every live ws belonging to a specific user across all projects.
|
|
1702
1783
|
// Used by user-targeted notifications (e.g. user-to-user @mentions).
|
|
1703
1784
|
function sendToUser(userId, msg) {
|
|
@@ -1813,6 +1894,7 @@ function createServer(opts) {
|
|
|
1813
1894
|
setRecovery: auth.setRecovery,
|
|
1814
1895
|
clearRecovery: auth.clearRecovery,
|
|
1815
1896
|
broadcastAll: broadcastAll,
|
|
1897
|
+
refreshProjectAccess: refreshProjectAccess,
|
|
1816
1898
|
forEachProject: forEachProject,
|
|
1817
1899
|
destroyProject: removeProject,
|
|
1818
1900
|
destroyAll: destroyAll,
|
|
@@ -42,6 +42,9 @@ function attachPairFactory(ctx) {
|
|
|
42
42
|
function validateVendor(vendor) {
|
|
43
43
|
var installed = sm.installedVendors || [];
|
|
44
44
|
if (!vendor || installed.indexOf(vendor) === -1) throw new Error("vendor is not installed: " + (vendor || "unknown"));
|
|
45
|
+
if (Array.isArray(sm.availableVendors) && sm.availableVendors.indexOf(vendor) === -1) {
|
|
46
|
+
throw new Error("vendor runtime is temporarily unavailable: " + vendor);
|
|
47
|
+
}
|
|
45
48
|
return vendor;
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
var MAX_RESPONSE_CHARS = 30000;
|
|
2
|
+
|
|
3
|
+
function responseText(history, fromIndex) {
|
|
4
|
+
var text = "";
|
|
5
|
+
for (var i = Math.max(0, fromIndex || 0); i < history.length; i++) {
|
|
6
|
+
if (history[i] && history[i].type === "delta" && history[i].text) text += history[i].text;
|
|
7
|
+
}
|
|
8
|
+
return text.length > MAX_RESPONSE_CHARS ? text.slice(-MAX_RESPONSE_CHARS) : text;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function errorSince(history, fromIndex) {
|
|
12
|
+
for (var i = history.length - 1; i >= Math.max(0, fromIndex || 0); i--) {
|
|
13
|
+
if (history[i] && history[i].type === "error") return history[i].text || "partner turn failed";
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function recentTurns(session, count) {
|
|
19
|
+
var history = session.history || [];
|
|
20
|
+
var starts = [];
|
|
21
|
+
for (var i = 0; i < history.length; i++) if (history[i] && history[i].type === "user_message") starts.push(i);
|
|
22
|
+
var from = starts.length > 0 ? starts[Math.max(0, starts.length - count)] : 0;
|
|
23
|
+
var turns = [];
|
|
24
|
+
var current = null;
|
|
25
|
+
for (var j = from; j < history.length; j++) {
|
|
26
|
+
var item = history[j];
|
|
27
|
+
if (!item) continue;
|
|
28
|
+
if (item.type === "user_message") {
|
|
29
|
+
current = { user: item.text || "", delegated: !!item.delegated, response: "" };
|
|
30
|
+
turns.push(current);
|
|
31
|
+
} else if (item.type === "delta" && item.text) {
|
|
32
|
+
if (!current) { current = { user: "", delegated: false, response: "" }; turns.push(current); }
|
|
33
|
+
current.response += item.text;
|
|
34
|
+
if (current.response.length > MAX_RESPONSE_CHARS) current.response = current.response.slice(-MAX_RESPONSE_CHARS);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return turns;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { errorSince: errorSince, recentTurns: recentTurns, responseText: responseText };
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// Partner-control tools for sessions that belong to a split group.
|
|
2
2
|
|
|
3
3
|
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
|
+
var z;
|
|
5
|
+
try { z = require("zod"); } catch (e) { z = null; }
|
|
6
|
+
var evaluationSchema = z ? z.union([
|
|
7
|
+
z.string(),
|
|
8
|
+
z.object({ outcome: z.string(), note: z.string().optional() }),
|
|
9
|
+
]) : null;
|
|
4
10
|
|
|
5
11
|
// `options.lifecycle` adds the autonomous management tools. They are omitted
|
|
6
12
|
// for a plain side-by-side split, which has no Driver role to exercise them.
|
|
@@ -15,6 +21,7 @@ function getToolDefs(handlers, options) {
|
|
|
15
21
|
wait: { type: "boolean", description: "Wait for the partner's turn to finish. Defaults to true." },
|
|
16
22
|
timeoutSeconds: { type: "number", description: "Maximum wait in seconds, from 1 to 900. Defaults to 300." },
|
|
17
23
|
operationId: { type: "string", description: "Optional stable id for this delegation. Reusing it within the same human turn returns the original operation instead of sending twice." },
|
|
24
|
+
taskId: { type: "string", description: "Optional stable task id for generation and completion correlation." },
|
|
18
25
|
}, ["message"]),
|
|
19
26
|
handler: function (args) { return handlers.send(args || {}); },
|
|
20
27
|
},
|
|
@@ -29,7 +36,7 @@ function getToolDefs(handlers, options) {
|
|
|
29
36
|
{
|
|
30
37
|
name: "interrupt_partner",
|
|
31
38
|
description: "Interrupt the Split 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 Split Worker returns any partial result to you for review.",
|
|
32
|
-
inputSchema: buildShape({}),
|
|
39
|
+
inputSchema: buildShape({ reason: { type: "string", description: "Why the Driver is interrupting this task." } }),
|
|
33
40
|
handler: function (args) { return handlers.interrupt(args || {}); },
|
|
34
41
|
},
|
|
35
42
|
{
|
|
@@ -41,6 +48,15 @@ function getToolDefs(handlers, options) {
|
|
|
41
48
|
];
|
|
42
49
|
if (!lifecycle) return defs;
|
|
43
50
|
return defs.concat([
|
|
51
|
+
{
|
|
52
|
+
name: "message_partner",
|
|
53
|
+
description: "Queue a non-interrupting message on the active Split Worker's runtime input without creating a delegated follow-up. Returns queued or rejected with an exact request id; queued does not claim model execution.",
|
|
54
|
+
inputSchema: buildShape({
|
|
55
|
+
message: { type: "string", description: "Bounded correction or context for the current active Worker turn." },
|
|
56
|
+
requestId: { type: "string", description: "Optional stable request id. Reuse it to deduplicate a retried delivery within this human turn." },
|
|
57
|
+
}, ["message"]),
|
|
58
|
+
handler: function (args) { return handlers.message(args || {}); },
|
|
59
|
+
},
|
|
44
60
|
{
|
|
45
61
|
name: "partner_status",
|
|
46
62
|
description: "Bounded capacity and continuity report for your Split Worker, for deciding reuse against replacement: context tokens used and the ratio of its window, current task and activity, vendor/model/effort, history size and idle time, whether it is safe to replace right now, and your own recorded results for earlier Worker generations. Returns no transcript. Prefer this over reading turns when you only need to decide.",
|
|
@@ -58,10 +74,11 @@ function getToolDefs(handlers, options) {
|
|
|
58
74
|
workerEffort: { type: "string", description: "Optional reasoning effort for the new Worker." },
|
|
59
75
|
recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended replacement vendor, model, and effort fit the next task." },
|
|
60
76
|
evaluation: {
|
|
61
|
-
|
|
77
|
+
schema: evaluationSchema,
|
|
62
78
|
description: "Optional bounded assessment of the Worker being replaced, recorded against that exact generation.",
|
|
63
79
|
},
|
|
64
80
|
operationId: { type: "string", description: "Optional stable id for this replacement. Reusing it within the same human turn returns the original operation instead of replacing twice." },
|
|
81
|
+
supersedeProposalId: { type: "string", description: "Exact pending replacement proposal id to supersede with this new configuration." },
|
|
65
82
|
}, ["message", "recommendationRationale"]),
|
|
66
83
|
handler: function (args) { return handlers.replace(args || {}); },
|
|
67
84
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function fingerprint(args, fields) {
|
|
2
|
+
var source = args || {};
|
|
3
|
+
var value = {};
|
|
4
|
+
for (var i = 0; i < fields.length; i++) {
|
|
5
|
+
var name = fields[i];
|
|
6
|
+
value[name] = Object.prototype.hasOwnProperty.call(source, name) ? source[name] : null;
|
|
7
|
+
}
|
|
8
|
+
return JSON.stringify(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { fingerprint: fingerprint };
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
// Pure data, extracted from project-session-pair.js so that module stays under
|
|
4
4
|
// the size limit and so the guidance can be reviewed as prose.
|
|
5
5
|
|
|
6
|
+
var WORKER_RUNTIME_SELECTION = [
|
|
7
|
+
"Use a high-low model mix: keep decomposition, judgment, and review with the Driver, and prefer a lighter",
|
|
8
|
+
"Worker model capable of the bounded execution task. Do not mirror the Driver's model or default to the",
|
|
9
|
+
"strongest available model merely because it is available. Explicitly fill the vendor, model, and thinking",
|
|
10
|
+
"effort recommendation fields for both creation and replacement; never leave model selection to card defaults.",
|
|
11
|
+
"Select model capability and thinking effort separately. Start with the lowest effort adequate for the task,",
|
|
12
|
+
"not high effort by habit. Choose from the installed, offered catalog; do not invent model availability or prices.",
|
|
13
|
+
"Explain why the lighter choice can handle the task. Escalate only for a concrete requirement beyond its",
|
|
14
|
+
"capabilities or observed failures, and state that reason. Use recorded Worker outcomes when available,",
|
|
15
|
+
"and reconsider a lighter model for the next bounded task after an escalation. Honor the user's explicit",
|
|
16
|
+
"runtime choice; this is a recommendation policy, not a restriction on which model the user may select.",
|
|
17
|
+
].join(" ").replace(/ {2,}/g, " ");
|
|
18
|
+
|
|
6
19
|
var DRIVER_DELEGATION = [
|
|
7
20
|
"Your management objective: protect your own context, keep the Split Worker compact, and put execution where",
|
|
8
21
|
"it runs best. Delegate implementation-heavy work rather than doing it here. Treat work spanning multiple modules,",
|
|
@@ -13,9 +26,10 @@ var DRIVER_DELEGATION = [
|
|
|
13
26
|
].join(" ").replace(/ {2,}/g, " ");
|
|
14
27
|
|
|
15
28
|
var DRIVER_CORE = [
|
|
29
|
+
WORKER_RUNTIME_SELECTION,
|
|
16
30
|
"You are the Driver of a visible Driver/Split Worker pair, and you manage that Split Worker yourself.",
|
|
17
|
-
"The tools
|
|
18
|
-
"and
|
|
31
|
+
"The pair tools distinguish a live message, a queued follow-up, an interrupt-and-replace task, resumption,",
|
|
32
|
+
"and replacement of the Worker runtime. Inspect exact task and proposal ids before changing queued work.",
|
|
19
33
|
"",
|
|
20
34
|
"Reuse the existing Split Worker",
|
|
21
35
|
"only when its accumulated context genuinely helps the next task; when it is context-bloated, stale, or",
|
|
@@ -34,6 +48,8 @@ var DRIVER_CORE = [
|
|
|
34
48
|
"interrupt true, which stops it first only after acceptance. A human Stop",
|
|
35
49
|
"is authoritative: do not retry, send more work, or replace the Worker in the same turn. Clay blocks those",
|
|
36
50
|
"actions until the human sends a new Driver message. Use close_partner when they ask to close the pane.",
|
|
51
|
+
"A Worker outcome envelope is Worker-reported evidence, not Driver verification. Preserve partial results and",
|
|
52
|
+
"correlate completions to their task and generation; never attach a late old-generation result to a new Worker.",
|
|
37
53
|
"",
|
|
38
54
|
"Recommend a Worker vendor, model, and effort from what is actually installed and offered, and include a concise",
|
|
39
55
|
"rationale explaining why all three fit the task. The card remains visible as an audit trail even when full access",
|
|
@@ -56,7 +72,12 @@ var DRIVER_CORE = [
|
|
|
56
72
|
|
|
57
73
|
var DRIVER = DRIVER_CORE + " " + DRIVER_DELEGATION;
|
|
58
74
|
|
|
75
|
+
function worker(taskId) {
|
|
76
|
+
return "You are the configured Split Worker. For task " + (taskId || "unknown") + ", call report_partner_outcome before finishing when the tool is available. Report only what you observed: changed files, verification commands/results, unverified work, running processes, and next action. Never claim Driver verification.";
|
|
77
|
+
}
|
|
78
|
+
|
|
59
79
|
var UNPAIRED = [
|
|
80
|
+
WORKER_RUNTIME_SELECTION,
|
|
60
81
|
"Infer the user's intended target from conversational and UI context. References to a user-visible paired or",
|
|
61
82
|
"split pane, its session, its activity or status, or a collaborator the user wants opened resolve to Clay's",
|
|
62
83
|
"Split Worker and its partner tools. Internal Sub-agents are a distinct execution mechanism, not a lexical",
|
|
@@ -76,4 +97,5 @@ module.exports = {
|
|
|
76
97
|
DRIVER_CORE: DRIVER_CORE,
|
|
77
98
|
DRIVER_DELEGATION: DRIVER_DELEGATION,
|
|
78
99
|
UNPAIRED: UNPAIRED,
|
|
100
|
+
worker: worker,
|
|
79
101
|
};
|
|
@@ -111,14 +111,19 @@ function attachPairTurnControl(ctx) {
|
|
|
111
111
|
return clean;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
function runOperation(driver, kind, rawId, fn) {
|
|
114
|
+
function runOperation(driver, kind, rawId, fn, fingerprint) {
|
|
115
115
|
var id = operationId(rawId);
|
|
116
116
|
if (!id) return Promise.resolve().then(fn);
|
|
117
117
|
var state = stateFor(driver);
|
|
118
118
|
var key = kind + ":" + id;
|
|
119
|
-
if (state.operations[key])
|
|
119
|
+
if (state.operations[key]) {
|
|
120
|
+
if (fingerprint !== undefined && state.operations[key].fingerprint !== fingerprint) {
|
|
121
|
+
return Promise.reject(new Error("operation id was already used with different input"));
|
|
122
|
+
}
|
|
123
|
+
return state.operations[key].promise;
|
|
124
|
+
}
|
|
120
125
|
var promise = Promise.resolve().then(fn);
|
|
121
|
-
state.operations[key] = promise;
|
|
126
|
+
state.operations[key] = { fingerprint: fingerprint, promise: promise };
|
|
122
127
|
return promise;
|
|
123
128
|
}
|
|
124
129
|
|
|
@@ -69,6 +69,7 @@ function restore(meta, session) {
|
|
|
69
69
|
createdVia: stored.createdVia === "split-worker" ? "split-worker" : null,
|
|
70
70
|
createdAt: typeof stored.createdAt === "number" && isFinite(stored.createdAt) ? stored.createdAt : null,
|
|
71
71
|
};
|
|
72
|
+
session._pairGeneration = session.sessionProvenance.generation;
|
|
72
73
|
return storedOriginId !== session.sessionOriginId || JSON.stringify(stored) !== JSON.stringify(session.sessionProvenance);
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -12,7 +12,8 @@ function buildShape(props, required) {
|
|
|
12
12
|
var key = keys[i];
|
|
13
13
|
var prop = props[key];
|
|
14
14
|
var field;
|
|
15
|
-
if (prop.
|
|
15
|
+
if (prop.schema) field = prop.schema;
|
|
16
|
+
else if (prop.type === "number") field = z.number();
|
|
16
17
|
else if (prop.type === "boolean") field = z.boolean();
|
|
17
18
|
else if (prop.enum) field = z.enum(prop.enum);
|
|
18
19
|
else field = z.string();
|