clay-server 3.4.0-beta.9 → 3.4.0
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 +58 -58
- package/lib/project-connection.js +19 -6
- package/lib/project-models.js +220 -0
- package/lib/project-session-handoff.js +162 -0
- package/lib/project-session-pair.js +14 -7
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +13 -45
- package/lib/project-worker-proposal.js +51 -14
- package/lib/project.js +33 -77
- package/lib/public/app.js +11 -0
- package/lib/public/copilot-avatar.svg +5 -0
- package/lib/public/css/filebrowser.css +6 -1
- package/lib/public/css/input.css +56 -0
- package/lib/public/css/mates.css +6 -0
- package/lib/public/css/menus.css +35 -56
- package/lib/public/css/pane.css +18 -3
- package/lib/public/css/session-actions.css +98 -0
- package/lib/public/grok-avatar.svg +4 -0
- package/lib/public/index.html +26 -9
- package/lib/public/junie-avatar.svg +11 -0
- package/lib/public/kimi-avatar.svg +4 -0
- package/lib/public/modules/agent-config-selects.js +69 -0
- package/lib/public/modules/app-connection.js +6 -0
- package/lib/public/modules/app-header.js +0 -22
- package/lib/public/modules/app-messages.js +48 -9
- package/lib/public/modules/app-panels.js +33 -82
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rendering.js +16 -1
- package/lib/public/modules/background-tasks-ui.js +55 -0
- package/lib/public/modules/filebrowser-tabs.js +65 -0
- package/lib/public/modules/filebrowser.js +23 -3
- package/lib/public/modules/mate-sidebar.js +10 -2
- package/lib/public/modules/model-picker.js +263 -0
- package/lib/public/modules/pane-bridge.js +6 -0
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/sidebar-mobile.js +2 -1
- package/lib/public/modules/sidebar-sessions.js +17 -1
- package/lib/public/modules/split-pair-ui.js +16 -77
- package/lib/public/modules/split-view.js +11 -1
- package/lib/public/modules/tools.js +5 -0
- package/lib/public/modules/vendor-priority.js +6 -1
- package/lib/public/modules/worker-proposal.js +6 -1
- package/lib/public/qwen-avatar.svg +4 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +57 -34
- package/lib/sdk-message-processor.js +26 -4
- package/lib/session-handoff-context.js +110 -0
- package/lib/session-notes-mcp-server.js +1 -0
- package/lib/sessions.js +4 -0
- package/lib/ws-schema.js +9 -1
- package/lib/yoke/acp-agent-profiles.js +64 -0
- package/lib/yoke/adapters/acp.js +2 -1
- package/lib/yoke/adapters/claude.js +34 -0
- package/lib/yoke/adapters/codex.js +38 -3
- package/lib/yoke/adapters/copilot.js +7 -0
- package/lib/yoke/adapters/grok.js +7 -0
- package/lib/yoke/adapters/junie.js +7 -0
- package/lib/yoke/adapters/kimi.js +7 -0
- package/lib/yoke/adapters/kiro.js +4 -3
- package/lib/yoke/adapters/qwen.js +7 -0
- package/lib/yoke/codex-background-tasks.js +84 -0
- package/lib/yoke/index.js +43 -14
- package/lib/yoke/instructions.js +3 -0
- package/lib/yoke/interface.js +21 -0
- package/lib/yoke/vendor-registry.js +55 -0
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -1041,64 +1041,6 @@ function shutdownProjects() {
|
|
|
1041
1041
|
|
|
1042
1042
|
// Worktree tracking extracted to daemon-projects.js
|
|
1043
1043
|
|
|
1044
|
-
// --- Register projects ---
|
|
1045
|
-
var projects = config.projects || [];
|
|
1046
|
-
for (var i = 0; i < projects.length; i++) {
|
|
1047
|
-
var p = projects[i];
|
|
1048
|
-
if (fs.existsSync(p.path)) {
|
|
1049
|
-
console.log("[daemon] Adding project:", p.slug, "→", p.path);
|
|
1050
|
-
relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId);
|
|
1051
|
-
// Discover and register worktrees for this project
|
|
1052
|
-
scanAndRegisterWorktrees(relay, p.path, p.slug, p.icon, p.ownerId);
|
|
1053
|
-
} else {
|
|
1054
|
-
console.log("[daemon] Skipping missing project:", p.path);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// Migrate legacy mates storage before registration
|
|
1059
|
-
mates.migrateLegacyMates();
|
|
1060
|
-
|
|
1061
|
-
// Register existing mates as projects
|
|
1062
|
-
if (usersModule.isMultiUser()) {
|
|
1063
|
-
// Multi-user mode: iterate over all users and load each user's mates
|
|
1064
|
-
var allUsers = usersModule.getAllUsers();
|
|
1065
|
-
for (var ui = 0; ui < allUsers.length; ui++) {
|
|
1066
|
-
var mateCtx = mates.buildMateCtx(allUsers[ui].id);
|
|
1067
|
-
var userMates = mates.getAllMates(mateCtx);
|
|
1068
|
-
for (var mi = 0; mi < userMates.length; mi++) {
|
|
1069
|
-
var m = userMates[mi];
|
|
1070
|
-
var mateDir = mates.getMateDir(mateCtx, m.id);
|
|
1071
|
-
var mateSlug = "mate-" + m.id;
|
|
1072
|
-
var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
|
|
1073
|
-
if (fs.existsSync(mateDir)) {
|
|
1074
|
-
var mateDef = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
|
|
1075
|
-
var isHost = !!(mateDef && mateDef.hostAgent);
|
|
1076
|
-
console.log("[daemon] Adding mate project:", mateSlug + (isHost ? " (host agent)" : ""));
|
|
1077
|
-
relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost });
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
} else {
|
|
1082
|
-
// Single-user mode: use null ctx
|
|
1083
|
-
var mateCtx = mates.buildMateCtx(null);
|
|
1084
|
-
var allMates = mates.getAllMates(mateCtx);
|
|
1085
|
-
for (var mi = 0; mi < allMates.length; mi++) {
|
|
1086
|
-
var m = allMates[mi];
|
|
1087
|
-
var mateDir = mates.getMateDir(mateCtx, m.id);
|
|
1088
|
-
var mateSlug = "mate-" + m.id;
|
|
1089
|
-
var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
|
|
1090
|
-
if (fs.existsSync(mateDir)) {
|
|
1091
|
-
var mateDef2 = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
|
|
1092
|
-
var isHost2 = !!(mateDef2 && mateDef2.hostAgent);
|
|
1093
|
-
console.log("[daemon] Adding mate project:", mateSlug + (isHost2 ? " (host agent)" : ""));
|
|
1094
|
-
relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost2 });
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
// Sync ~/.clayrc on startup
|
|
1100
|
-
try { syncClayrc(config.projects); } catch (e) {}
|
|
1101
|
-
|
|
1102
1044
|
// --- IPC server ---
|
|
1103
1045
|
// Clean up stale socket/config left by a previously killed daemon
|
|
1104
1046
|
var existingConfig = loadConfig();
|
|
@@ -1334,6 +1276,64 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1334
1276
|
}
|
|
1335
1277
|
});
|
|
1336
1278
|
|
|
1279
|
+
// --- Register projects ---
|
|
1280
|
+
var projects = config.projects || [];
|
|
1281
|
+
for (var i = 0; i < projects.length; i++) {
|
|
1282
|
+
var p = projects[i];
|
|
1283
|
+
if (fs.existsSync(p.path)) {
|
|
1284
|
+
console.log("[daemon] Adding project:", p.slug, "→", p.path);
|
|
1285
|
+
relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId);
|
|
1286
|
+
// Discover and register worktrees for this project
|
|
1287
|
+
scanAndRegisterWorktrees(relay, p.path, p.slug, p.icon, p.ownerId);
|
|
1288
|
+
} else {
|
|
1289
|
+
console.log("[daemon] Skipping missing project:", p.path);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Migrate legacy mates storage before registration
|
|
1294
|
+
mates.migrateLegacyMates();
|
|
1295
|
+
|
|
1296
|
+
// Register existing mates as projects
|
|
1297
|
+
if (usersModule.isMultiUser()) {
|
|
1298
|
+
// Multi-user mode: iterate over all users and load each user's mates
|
|
1299
|
+
var allUsers = usersModule.getAllUsers();
|
|
1300
|
+
for (var ui = 0; ui < allUsers.length; ui++) {
|
|
1301
|
+
var mateCtx = mates.buildMateCtx(allUsers[ui].id);
|
|
1302
|
+
var userMates = mates.getAllMates(mateCtx);
|
|
1303
|
+
for (var mi = 0; mi < userMates.length; mi++) {
|
|
1304
|
+
var m = userMates[mi];
|
|
1305
|
+
var mateDir = mates.getMateDir(mateCtx, m.id);
|
|
1306
|
+
var mateSlug = "mate-" + m.id;
|
|
1307
|
+
var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
|
|
1308
|
+
if (fs.existsSync(mateDir)) {
|
|
1309
|
+
var mateDef = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
|
|
1310
|
+
var isHost = !!(mateDef && mateDef.hostAgent);
|
|
1311
|
+
console.log("[daemon] Adding mate project:", mateSlug + (isHost ? " (host agent)" : ""));
|
|
1312
|
+
relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost });
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
} else {
|
|
1317
|
+
// Single-user mode: use null ctx
|
|
1318
|
+
var mateCtx = mates.buildMateCtx(null);
|
|
1319
|
+
var allMates = mates.getAllMates(mateCtx);
|
|
1320
|
+
for (var mi = 0; mi < allMates.length; mi++) {
|
|
1321
|
+
var m = allMates[mi];
|
|
1322
|
+
var mateDir = mates.getMateDir(mateCtx, m.id);
|
|
1323
|
+
var mateSlug = "mate-" + m.id;
|
|
1324
|
+
var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
|
|
1325
|
+
if (fs.existsSync(mateDir)) {
|
|
1326
|
+
var mateDef2 = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
|
|
1327
|
+
var isHost2 = !!(mateDef2 && mateDef2.hostAgent);
|
|
1328
|
+
console.log("[daemon] Adding mate project:", mateSlug + (isHost2 ? " (host agent)" : ""));
|
|
1329
|
+
relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost2 });
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// Sync ~/.clayrc on startup
|
|
1335
|
+
try { syncClayrc(config.projects); } catch (e) {}
|
|
1336
|
+
|
|
1337
1337
|
// --- Cleanup stale SDK warmup CLI session files ---
|
|
1338
1338
|
//
|
|
1339
1339
|
// On adapter init the SDK is given a dummy "hi" prompt to discover models,
|
|
@@ -22,12 +22,23 @@ function dispatchMessageSafely(handleMessage, sendTo, slug, ws, msg) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function buildInitialModelInfo(sm, initialVendor, initialModel, initialModels) {
|
|
25
|
+
function buildInitialModelInfo(sm, initialVendor, initialModel, initialModels, sessionId) {
|
|
26
|
+
var model = initialModel || "";
|
|
27
|
+
var models = initialModels || [];
|
|
28
|
+
if (model && models.length > 0) {
|
|
29
|
+
var found = false;
|
|
30
|
+
for (var i = 0; i < models.length; i++) {
|
|
31
|
+
var entry = models[i];
|
|
32
|
+
if (typeof entry === "string" ? entry === model : (entry.value === model || entry.id === model || entry.resolvedModel === model)) found = true;
|
|
33
|
+
}
|
|
34
|
+
if (!found) model = typeof models[0] === "string" ? models[0] : (models[0].value || models[0].id || "");
|
|
35
|
+
}
|
|
26
36
|
return {
|
|
27
37
|
type: "model_info",
|
|
28
|
-
model:
|
|
29
|
-
models:
|
|
38
|
+
model: model,
|
|
39
|
+
models: models,
|
|
30
40
|
vendor: initialVendor,
|
|
41
|
+
sessionId: sessionId || null,
|
|
31
42
|
availableVendors: sm.availableVendors || [],
|
|
32
43
|
installedVendors: sm.installedVendors || [],
|
|
33
44
|
};
|
|
@@ -177,11 +188,12 @@ function attachConnection(ctx) {
|
|
|
177
188
|
if (sm.slashCommands) {
|
|
178
189
|
sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
|
|
179
190
|
}
|
|
180
|
-
var initialModel = (restoredActive && restoredActive.model) || sm.
|
|
191
|
+
var initialModel = (restoredActive && restoredActive.model) || ((sm.defaultModelByVendor || {})[initialVendor]) || "";
|
|
181
192
|
// Vendor installation state is needed even before a new project has a
|
|
182
193
|
// model. Always send it so reconnects cannot fall back to "Not installed."
|
|
183
|
-
|
|
184
|
-
sendTo(ws,
|
|
194
|
+
var initialModelInfo = buildInitialModelInfo(sm, initialVendor, initialModel, initialModels, restoredActive ? restoredActive.localId : null);
|
|
195
|
+
sendTo(ws, initialModelInfo);
|
|
196
|
+
sendTo(ws, { type: "config_state", model: initialModelInfo.model, vendor: initialVendor, sessionId: restoredActive ? restoredActive.localId : null, mode: sm.currentPermissionMode || "default", effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
185
197
|
sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
|
|
186
198
|
sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
|
|
187
199
|
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
@@ -282,6 +294,7 @@ function attachConnection(ctx) {
|
|
|
282
294
|
if (active.isProcessing) {
|
|
283
295
|
sendTo(ws, { type: "status", status: "processing" });
|
|
284
296
|
}
|
|
297
|
+
sendTo(ws, { type: "active_background_tasks", tasks: active.activeBackgroundTasks || [] });
|
|
285
298
|
var pendingIds = Object.keys(active.pendingPermissions);
|
|
286
299
|
for (var pi = 0; pi < pendingIds.length; pi++) {
|
|
287
300
|
var p = active.pendingPermissions[pendingIds[pi]];
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
var yoke = require("./yoke");
|
|
2
|
+
|
|
3
|
+
function modelEntryValue(entry) {
|
|
4
|
+
if (!entry) return "";
|
|
5
|
+
if (typeof entry === "string") return entry;
|
|
6
|
+
return entry.value || entry.id || "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function modelEntryMatches(entry, value) {
|
|
10
|
+
if (!entry || !value) return false;
|
|
11
|
+
if (typeof entry === "string") return entry === value;
|
|
12
|
+
return entry.value === value || entry.id === value || entry.resolvedModel === value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function attachModels(ctx) {
|
|
16
|
+
var cwd = ctx.cwd;
|
|
17
|
+
var slug = ctx.slug;
|
|
18
|
+
var sm = ctx.sm;
|
|
19
|
+
var sdk = ctx.sdk;
|
|
20
|
+
var adapters = ctx.adapters;
|
|
21
|
+
var sendTo = ctx.sendTo;
|
|
22
|
+
var getSessionForWs = ctx.getSessionForWs;
|
|
23
|
+
var getLinuxUserForWs = ctx.getLinuxUserForWs;
|
|
24
|
+
var serverPort = ctx.serverPort;
|
|
25
|
+
var serverTls = ctx.serverTls;
|
|
26
|
+
var serverAuthToken = ctx.serverAuthToken;
|
|
27
|
+
|
|
28
|
+
async function loadVendorModels(ws, msg) {
|
|
29
|
+
var vendor = msg.vendor || "";
|
|
30
|
+
var requestId = msg.requestId || null;
|
|
31
|
+
var loadError = null;
|
|
32
|
+
var vendorAdapter = null;
|
|
33
|
+
var session = getSessionForWs(ws);
|
|
34
|
+
|
|
35
|
+
if (!vendor) {
|
|
36
|
+
sendTo(ws, {
|
|
37
|
+
type: "model_info",
|
|
38
|
+
vendor: vendor,
|
|
39
|
+
requestId: requestId,
|
|
40
|
+
modelStatus: "error",
|
|
41
|
+
error: "No vendor was selected.",
|
|
42
|
+
model: "",
|
|
43
|
+
models: [],
|
|
44
|
+
sessionId: session ? session.localId : null,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
var modelLinuxUser = getLinuxUserForWs(ws);
|
|
51
|
+
vendorAdapter = adapters[vendor] || null;
|
|
52
|
+
if (!vendorAdapter) {
|
|
53
|
+
vendorAdapter = await yoke.lazyCreateAdapter(adapters, vendor, {
|
|
54
|
+
cwd: cwd,
|
|
55
|
+
linuxUser: modelLinuxUser || undefined,
|
|
56
|
+
clayPort: serverPort,
|
|
57
|
+
clayTls: serverTls,
|
|
58
|
+
clayAuthToken: serverAuthToken,
|
|
59
|
+
slug: slug,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var cachedModels = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
|
|
64
|
+
var cachedCapabilities = sm.capabilitiesByVendor && sm.capabilitiesByVendor[vendor];
|
|
65
|
+
var needsReadyMetadata = !cachedCapabilities || cachedModels.length === 0;
|
|
66
|
+
if (vendorAdapter && needsReadyMetadata && typeof vendorAdapter.init === "function") {
|
|
67
|
+
try {
|
|
68
|
+
var readyResult = await vendorAdapter.init({
|
|
69
|
+
cwd: cwd,
|
|
70
|
+
linuxUser: modelLinuxUser || undefined,
|
|
71
|
+
clayPort: serverPort,
|
|
72
|
+
clayTls: serverTls,
|
|
73
|
+
clayAuthToken: serverAuthToken,
|
|
74
|
+
slug: slug,
|
|
75
|
+
});
|
|
76
|
+
sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
|
|
77
|
+
sm.capabilitiesByVendor[vendor] = readyResult.capabilities || {};
|
|
78
|
+
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
79
|
+
if (Array.isArray(readyResult.models) && readyResult.models.length > 0) {
|
|
80
|
+
sm.modelsByVendor[vendor] = readyResult.models;
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
loadError = e;
|
|
84
|
+
console.error("[project-models] " + vendor + " init failed:", e.message || e);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (vendorAdapter) {
|
|
89
|
+
sm.availableVendors = Object.keys(adapters);
|
|
90
|
+
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
91
|
+
var currentModels = sm.modelsByVendor[vendor] || [];
|
|
92
|
+
if (currentModels.length === 0 && typeof vendorAdapter.supportedModels === "function") {
|
|
93
|
+
try {
|
|
94
|
+
var supported = await vendorAdapter.supportedModels();
|
|
95
|
+
if (Array.isArray(supported) && supported.length > 0) {
|
|
96
|
+
sm.modelsByVendor[vendor] = supported;
|
|
97
|
+
loadError = null;
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if (!loadError) loadError = e;
|
|
101
|
+
console.error("[project-models] " + vendor + " model listing failed:", e.message || e);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
loadError = new Error("The vendor adapter is unavailable.");
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
loadError = e;
|
|
109
|
+
console.error("[project-models] model loading failed for " + vendor + ":", e.message || e);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
|
|
113
|
+
var modelToSend = modelEntryValue(vendorModels[0]);
|
|
114
|
+
var preferredModel = (session && session.vendor === vendor && session.model) || (sm.defaultModelByVendor && sm.defaultModelByVendor[vendor]) || "";
|
|
115
|
+
if (preferredModel) {
|
|
116
|
+
for (var i = 0; i < vendorModels.length; i++) {
|
|
117
|
+
if (modelEntryMatches(vendorModels[i], preferredModel)) {
|
|
118
|
+
modelToSend = modelEntryValue(vendorModels[i]);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
var vendorInfo = yoke.getVendorInfo(vendor);
|
|
124
|
+
var displayName = vendorInfo ? vendorInfo.displayName : vendor;
|
|
125
|
+
var status = vendorModels.length > 0 ? "ready" : (loadError ? "error" : "empty");
|
|
126
|
+
var errorText = "";
|
|
127
|
+
if (status === "error") {
|
|
128
|
+
errorText = "Could not load " + displayName + " models: " + (loadError.message || loadError);
|
|
129
|
+
} else if (status === "empty") {
|
|
130
|
+
errorText = displayName + " returned no models. Check CLI authentication and retry.";
|
|
131
|
+
}
|
|
132
|
+
sendTo(ws, {
|
|
133
|
+
type: "model_info",
|
|
134
|
+
model: modelToSend,
|
|
135
|
+
models: vendorModels,
|
|
136
|
+
vendor: vendor,
|
|
137
|
+
sessionId: session ? session.localId : null,
|
|
138
|
+
capabilities: (sm.capabilitiesByVendor && sm.capabilitiesByVendor[vendor]) || {},
|
|
139
|
+
availableVendors: sm.availableVendors || [],
|
|
140
|
+
installedVendors: sm.installedVendors || [],
|
|
141
|
+
requestId: requestId,
|
|
142
|
+
modelStatus: status,
|
|
143
|
+
error: errorText,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function selectModel(ws, msg) {
|
|
148
|
+
var session = getSessionForWs(ws);
|
|
149
|
+
if (!session) {
|
|
150
|
+
sendTo(ws, { type: "model_selection_result", requestId: msg.requestId || null, ok: false, error: "No active session." });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
var sessionVendor = session.vendor || sm.defaultVendor || "claude";
|
|
154
|
+
if (msg.vendor && msg.vendor !== sessionVendor) {
|
|
155
|
+
sendTo(ws, {
|
|
156
|
+
type: "model_selection_result",
|
|
157
|
+
requestId: msg.requestId || null,
|
|
158
|
+
ok: false,
|
|
159
|
+
error: "The session vendor changed before the model selection completed.",
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
var knownModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
|
|
164
|
+
if (knownModels.length > 0) {
|
|
165
|
+
var known = false;
|
|
166
|
+
for (var i = 0; i < knownModels.length; i++) {
|
|
167
|
+
if (modelEntryMatches(knownModels[i], msg.model)) {
|
|
168
|
+
known = true;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!known) {
|
|
173
|
+
sendTo(ws, {
|
|
174
|
+
type: "model_selection_result",
|
|
175
|
+
requestId: msg.requestId || null,
|
|
176
|
+
ok: false,
|
|
177
|
+
error: "That model is not available for the selected vendor.",
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
var result = await sdk.setModel(session, msg.model);
|
|
183
|
+
sendTo(ws, {
|
|
184
|
+
type: "model_selection_result",
|
|
185
|
+
requestId: msg.requestId || null,
|
|
186
|
+
ok: !!(result && result.ok),
|
|
187
|
+
model: result && result.model ? result.model : msg.model,
|
|
188
|
+
vendor: sessionVendor,
|
|
189
|
+
error: result && result.error ? result.error : "",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function handleMessage(ws, msg) {
|
|
194
|
+
if (msg.type === "get_vendor_models") {
|
|
195
|
+
loadVendorModels(ws, msg).catch(function(e) {
|
|
196
|
+
console.error("[project-models] Unexpected model loading failure:", e.message || e);
|
|
197
|
+
});
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (msg.type === "set_model" && msg.model) {
|
|
201
|
+
selectModel(ws, msg).catch(function(e) {
|
|
202
|
+
sendTo(ws, { type: "model_selection_result", requestId: msg.requestId || null, ok: false, error: e.message || String(e) });
|
|
203
|
+
});
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
handleMessage: handleMessage,
|
|
211
|
+
loadVendorModels: loadVendorModels,
|
|
212
|
+
selectModel: selectModel,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
attachModels: attachModels,
|
|
218
|
+
modelEntryMatches: modelEntryMatches,
|
|
219
|
+
modelEntryValue: modelEntryValue,
|
|
220
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
var yoke = require("./yoke");
|
|
2
|
+
var contextBuilder = require("./session-handoff-context");
|
|
3
|
+
|
|
4
|
+
function hasUserContext(session) {
|
|
5
|
+
var history = (session && session.history) || [];
|
|
6
|
+
for (var i = 0; i < history.length; i++) {
|
|
7
|
+
if (history[i] && history[i].type === "user_message" && history[i].text) return true;
|
|
8
|
+
if (history[i] && history[i].type === "handoff_context" && history[i].request) return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function attachSessionHandoff(ctx) {
|
|
14
|
+
var sm = ctx.sm;
|
|
15
|
+
|
|
16
|
+
function sendResult(ws, ok, details) {
|
|
17
|
+
ctx.sendTo(ws, Object.assign({ type: "session_handoff_result", ok: ok }, details || {}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validate(ws, source, targetVendor) {
|
|
21
|
+
if (ctx.isMate) throw new Error("Session handoff is only available in projects.");
|
|
22
|
+
if (!source) throw new Error("No active session was found.");
|
|
23
|
+
if (ctx.splitStore && ctx.splitStore.groupForMember(source.localId)) {
|
|
24
|
+
throw new Error("Open the session by itself before continuing it in another agent.");
|
|
25
|
+
}
|
|
26
|
+
if (source.isProcessing || source._queryStarting) {
|
|
27
|
+
throw new Error("Wait for the current response to finish before continuing in another agent.");
|
|
28
|
+
}
|
|
29
|
+
if (!hasUserContext(source)) throw new Error("This session does not have any conversation to continue.");
|
|
30
|
+
if (!targetVendor) throw new Error("Choose an agent for the handoff.");
|
|
31
|
+
if (!ctx.adapters[targetVendor] || (sm.installedVendors || []).indexOf(targetVendor) === -1) {
|
|
32
|
+
throw new Error("The selected coding agent is not installed.");
|
|
33
|
+
}
|
|
34
|
+
var linuxUser = ctx.getLinuxUserForSession(source);
|
|
35
|
+
var vendorInfo = yoke.getVendorInfo(targetVendor);
|
|
36
|
+
if (linuxUser && vendorInfo && vendorInfo.osUserIsolation === false) {
|
|
37
|
+
throw new Error((vendorInfo.displayName || targetVendor) + " is not available for OS-isolated users.");
|
|
38
|
+
}
|
|
39
|
+
var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
|
|
40
|
+
if ((source.ownerId || null) !== ownerId) throw new Error("Session access denied.");
|
|
41
|
+
return linuxUser;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function quietRecord(session, entry) {
|
|
45
|
+
session.history.push(entry);
|
|
46
|
+
sm.appendToSessionFile(session, entry);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handoff(ws, msg) {
|
|
50
|
+
var source = sm.sessions.get(ws._clayActiveSession) || null;
|
|
51
|
+
var targetVendor = typeof msg.targetVendor === "string" ? msg.targetVendor.trim() : "";
|
|
52
|
+
var targetModel = typeof msg.model === "string" ? msg.model.trim() : "";
|
|
53
|
+
var linuxUser;
|
|
54
|
+
try {
|
|
55
|
+
linuxUser = validate(ws, source, targetVendor);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
sendResult(ws, false, { error: err.message || String(err) });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var targetEffort = yoke.clampEffort(
|
|
62
|
+
targetVendor,
|
|
63
|
+
msg.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[targetVendor]) || sm.currentEffort || "medium"
|
|
64
|
+
) || null;
|
|
65
|
+
var target = sm.createSessionRaw({
|
|
66
|
+
ownerId: source.ownerId || null,
|
|
67
|
+
sessionVisibility: source.sessionVisibility || "shared",
|
|
68
|
+
vendor: targetVendor,
|
|
69
|
+
model: targetModel || null,
|
|
70
|
+
effort: targetEffort,
|
|
71
|
+
});
|
|
72
|
+
var targetName = (yoke.getVendorInfo(targetVendor) || {}).displayName || targetVendor;
|
|
73
|
+
target.title = (source.title || "Continued session") + " · " + targetName;
|
|
74
|
+
target.handoff = {
|
|
75
|
+
sourceSessionId: source.localId,
|
|
76
|
+
sourceVendor: source.vendor || sm.defaultVendor || "claude",
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
mode: "context",
|
|
79
|
+
sourceHistoryIndex: source.history.length,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
var sourceEntry = {
|
|
83
|
+
type: "handoff_created",
|
|
84
|
+
targetSessionId: target.localId,
|
|
85
|
+
targetVendor: targetVendor,
|
|
86
|
+
targetTitle: target.title,
|
|
87
|
+
_ts: Date.now(),
|
|
88
|
+
};
|
|
89
|
+
var targetEntry = {
|
|
90
|
+
type: "handoff_context",
|
|
91
|
+
sourceSessionId: source.localId,
|
|
92
|
+
sourceVendor: target.handoff.sourceVendor,
|
|
93
|
+
sourceTitle: source.title || "Untitled session",
|
|
94
|
+
targetVendor: targetVendor,
|
|
95
|
+
request: contextBuilder.latestUserRequest(source.history),
|
|
96
|
+
_ts: Date.now(),
|
|
97
|
+
};
|
|
98
|
+
quietRecord(source, sourceEntry);
|
|
99
|
+
quietRecord(target, targetEntry);
|
|
100
|
+
|
|
101
|
+
var prompt = contextBuilder.buildHandoffContext({
|
|
102
|
+
cwd: ctx.cwd,
|
|
103
|
+
source: source,
|
|
104
|
+
targetVendor: targetVendor,
|
|
105
|
+
});
|
|
106
|
+
target.isProcessing = true;
|
|
107
|
+
target.lastActivity = Date.now();
|
|
108
|
+
target.sentToolResults = {};
|
|
109
|
+
sm.switchSession(target.localId, ws);
|
|
110
|
+
if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
|
|
111
|
+
sendResult(ws, true, {
|
|
112
|
+
sourceSessionId: source.localId,
|
|
113
|
+
targetSessionId: target.localId,
|
|
114
|
+
targetVendor: targetVendor,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
var sdk = ctx.getSdk();
|
|
118
|
+
function failStart(err) {
|
|
119
|
+
target.isProcessing = false;
|
|
120
|
+
sm.sendAndRecord(target, { type: "error", text: "Could not start the handoff session: " + (err.message || String(err)) });
|
|
121
|
+
if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
|
|
122
|
+
sm.broadcastSessionList();
|
|
123
|
+
}
|
|
124
|
+
if (!sdk || typeof sdk.startQuery !== "function") {
|
|
125
|
+
failStart(new Error("SDK bridge is not ready."));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
target._queryStartTs = Date.now();
|
|
129
|
+
var startPromise;
|
|
130
|
+
try {
|
|
131
|
+
startPromise = sdk.startQuery(target, prompt, undefined, linuxUser);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
failStart(err);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
Promise.resolve(startPromise).catch(failStart);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function handleMessage(ws, msg) {
|
|
140
|
+
if (msg.type === "handoff_session_options") {
|
|
141
|
+
ctx.sendTo(ws, {
|
|
142
|
+
type: "handoff_session_options",
|
|
143
|
+
installedVendors: sm.installedVendors || [],
|
|
144
|
+
modelsByVendor: sm.modelsByVendor || {},
|
|
145
|
+
capabilitiesByVendor: sm.capabilitiesByVendor || {},
|
|
146
|
+
});
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (msg.type === "handoff_session") {
|
|
150
|
+
handoff(ws, msg);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { handleMessage: handleMessage };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
attachSessionHandoff: attachSessionHandoff,
|
|
161
|
+
hasUserContext: hasUserContext,
|
|
162
|
+
};
|
|
@@ -98,10 +98,15 @@ function attachSessionPair(ctx) {
|
|
|
98
98
|
token.delivered = true;
|
|
99
99
|
var failure = token.failure || null;
|
|
100
100
|
var response = token.response || "";
|
|
101
|
-
var text
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
var text;
|
|
102
|
+
if (token.interrupted) {
|
|
103
|
+
text = "[Worker execution interrupted] The user interrupted the Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
|
|
104
|
+
} else {
|
|
105
|
+
text = "A Worker task delegated through send_to_partner has finished.\n\n" +
|
|
106
|
+
"Original task:\n" + token.message + "\n\n" +
|
|
107
|
+
(failure ? "Worker error:\n" + failure : "Worker result:\n" + (response || "(No text response was recorded.)")) +
|
|
108
|
+
"\n\nReview the result, verify it as needed, and continue the task.";
|
|
109
|
+
}
|
|
105
110
|
sm.sendAndRecord(caller, {
|
|
106
111
|
type: "user_message",
|
|
107
112
|
text: text,
|
|
@@ -146,6 +151,7 @@ function attachSessionPair(ctx) {
|
|
|
146
151
|
}
|
|
147
152
|
token.response = responseText(partner.history || [], token.startIndex);
|
|
148
153
|
token.failure = errorSince(partner.history || [], token.startIndex);
|
|
154
|
+
token.interrupted = !token.failure && !!partner._lastTurnInterrupted;
|
|
149
155
|
finishDelegation(group, caller, partner, token);
|
|
150
156
|
return resumeDriverWithResult(caller, partner, token);
|
|
151
157
|
}
|
|
@@ -165,8 +171,9 @@ function attachSessionPair(ctx) {
|
|
|
165
171
|
clearInterval(timer);
|
|
166
172
|
finishDelegation(group, caller, partner, token);
|
|
167
173
|
var failure = errorSince(partner.history || [], token.startIndex);
|
|
174
|
+
var interrupted = !failure && !!partner._lastTurnInterrupted;
|
|
168
175
|
resolve({
|
|
169
|
-
status: failure ? "error" : "complete",
|
|
176
|
+
status: failure ? "error" : (interrupted ? "interrupted" : "complete"),
|
|
170
177
|
response: responseText(partner.history || [], token.startIndex),
|
|
171
178
|
error: failure || undefined,
|
|
172
179
|
});
|
|
@@ -260,7 +267,7 @@ function attachSessionPair(ctx) {
|
|
|
260
267
|
var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
|
|
261
268
|
count = Math.max(1, Math.min(5, count));
|
|
262
269
|
return toolResult({
|
|
263
|
-
status: resolved.partner.isProcessing ? "running" : "idle",
|
|
270
|
+
status: resolved.partner.isProcessing ? "running" : (resolved.partner._lastTurnInterrupted ? "interrupted" : "idle"),
|
|
264
271
|
partnerId: resolved.partner.localId,
|
|
265
272
|
title: resolved.partner.title || "New Session",
|
|
266
273
|
turns: recentTurns(resolved.partner, count),
|
|
@@ -383,7 +390,7 @@ function attachSessionPair(ctx) {
|
|
|
383
390
|
var group = store.groupForMember(session.localId);
|
|
384
391
|
var pairPrompt = "";
|
|
385
392
|
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. A completed Worker turn leaves the Worker session available for more work. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If send_to_partner reports that the Worker is no longer available, create a replacement with spawn_sessions and delegate the remaining implementation rather than taking it over. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
|
|
393
|
+
pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner and read_partner are provided directly to you. Use send_to_partner to delegate concrete bounded work to the Worker. A completed Worker turn leaves the Worker session available for more work. If a Worker turn is interrupted, its work is partial and unverified; review it and decide next steps with the user. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If send_to_partner reports that the Worker is no longer available, create a replacement with spawn_sessions and delegate the remaining implementation rather than taking it over. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
|
|
387
394
|
}
|
|
388
395
|
return [pairPrompt, workerProposal.getSystemPrompt(session)].filter(Boolean).join("\n\n");
|
|
389
396
|
}
|
|
@@ -319,7 +319,7 @@ function attachSessionSpawn(ctx) {
|
|
|
319
319
|
statuses.push({
|
|
320
320
|
localId: session.localId,
|
|
321
321
|
title: session.title || "New Session",
|
|
322
|
-
status: session.isProcessing ? "running" : (hasSessionError(session) ? "error" : "done"),
|
|
322
|
+
status: session.isProcessing ? "running" : (session._lastTurnInterrupted ? "interrupted" : (hasSessionError(session) ? "error" : "done")),
|
|
323
323
|
turnCount: session.turnCount || 0,
|
|
324
324
|
lastActivity: session.lastActivity || session.createdAt || 0,
|
|
325
325
|
});
|