clay-server 3.4.0-beta.9 → 3.5.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/lib/daemon.js +58 -58
  2. package/lib/project-connection.js +19 -6
  3. package/lib/project-models.js +220 -0
  4. package/lib/project-session-handoff.js +281 -0
  5. package/lib/project-session-pair.js +14 -7
  6. package/lib/project-session-spawn.js +1 -1
  7. package/lib/project-sessions.js +13 -45
  8. package/lib/project-worker-proposal.js +51 -14
  9. package/lib/project.js +74 -80
  10. package/lib/public/app.js +11 -0
  11. package/lib/public/copilot-avatar.svg +5 -0
  12. package/lib/public/css/filebrowser.css +6 -1
  13. package/lib/public/css/input.css +56 -0
  14. package/lib/public/css/mates.css +6 -0
  15. package/lib/public/css/menus.css +35 -56
  16. package/lib/public/css/pane.css +18 -3
  17. package/lib/public/css/session-actions.css +98 -0
  18. package/lib/public/grok-avatar.svg +4 -0
  19. package/lib/public/index.html +26 -9
  20. package/lib/public/junie-avatar.svg +11 -0
  21. package/lib/public/kimi-avatar.svg +4 -0
  22. package/lib/public/modules/agent-config-selects.js +69 -0
  23. package/lib/public/modules/app-connection.js +6 -0
  24. package/lib/public/modules/app-header.js +0 -22
  25. package/lib/public/modules/app-messages.js +48 -9
  26. package/lib/public/modules/app-panels.js +33 -82
  27. package/lib/public/modules/app-projects.js +2 -0
  28. package/lib/public/modules/app-rendering.js +16 -1
  29. package/lib/public/modules/background-tasks-ui.js +55 -0
  30. package/lib/public/modules/filebrowser-tabs.js +65 -0
  31. package/lib/public/modules/filebrowser.js +23 -3
  32. package/lib/public/modules/mate-sidebar.js +10 -2
  33. package/lib/public/modules/model-picker.js +263 -0
  34. package/lib/public/modules/pane-bridge.js +6 -0
  35. package/lib/public/modules/session-actions.js +294 -0
  36. package/lib/public/modules/sidebar-mobile.js +2 -1
  37. package/lib/public/modules/sidebar-sessions.js +17 -1
  38. package/lib/public/modules/split-pair-ui.js +16 -77
  39. package/lib/public/modules/split-view.js +11 -1
  40. package/lib/public/modules/tools.js +5 -0
  41. package/lib/public/modules/vendor-priority.js +6 -1
  42. package/lib/public/modules/worker-proposal.js +6 -1
  43. package/lib/public/qwen-avatar.svg +4 -0
  44. package/lib/public/style.css +1 -0
  45. package/lib/sdk-bridge.js +57 -34
  46. package/lib/sdk-message-processor.js +26 -4
  47. package/lib/session-handoff-context.js +113 -0
  48. package/lib/session-handoff-mcp-server.js +36 -0
  49. package/lib/session-notes-mcp-server.js +1 -0
  50. package/lib/sessions.js +4 -0
  51. package/lib/ws-schema.js +9 -1
  52. package/lib/yoke/acp-agent-profiles.js +64 -0
  53. package/lib/yoke/adapters/acp.js +2 -1
  54. package/lib/yoke/adapters/claude.js +34 -0
  55. package/lib/yoke/adapters/codex.js +38 -3
  56. package/lib/yoke/adapters/copilot.js +7 -0
  57. package/lib/yoke/adapters/grok.js +7 -0
  58. package/lib/yoke/adapters/junie.js +7 -0
  59. package/lib/yoke/adapters/kimi.js +7 -0
  60. package/lib/yoke/adapters/kiro.js +4 -3
  61. package/lib/yoke/adapters/qwen.js +7 -0
  62. package/lib/yoke/codex-background-tasks.js +84 -0
  63. package/lib/yoke/index.js +43 -14
  64. package/lib/yoke/instructions.js +3 -0
  65. package/lib/yoke/interface.js +21 -0
  66. package/lib/yoke/vendor-registry.js +66 -0
  67. 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: initialModel || "",
29
- models: initialModels || [],
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.currentModel || "";
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
- sendTo(ws, buildInitialModelInfo(sm, initialVendor, initialModel, initialModels));
184
- sendTo(ws, { type: "config_state", model: initialModel, mode: sm.currentPermissionMode || "default", effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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,281 @@
1
+ var yoke = require("./yoke");
2
+ var contextBuilder = require("./session-handoff-context");
3
+ var sessionHandoffMcp = require("./session-handoff-mcp-server");
4
+
5
+ var MAX_HANDOFF_CHAIN_DEPTH = 5;
6
+
7
+ function toolResult(text) {
8
+ return Promise.resolve({ content: [{ type: "text", text: text }] });
9
+ }
10
+
11
+ function toolError(message) {
12
+ return Promise.resolve({
13
+ content: [{ type: "text", text: "Error: " + message }],
14
+ isError: true,
15
+ });
16
+ }
17
+
18
+ function hasUserContext(session) {
19
+ var history = (session && session.history) || [];
20
+ for (var i = 0; i < history.length; i++) {
21
+ if (history[i] && history[i].type === "user_message" && history[i].text) return true;
22
+ if (history[i] && history[i].type === "handoff_context" && history[i].request) return true;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ function attachSessionHandoff(ctx) {
28
+ var sm = ctx.sm;
29
+
30
+ function sendResult(ws, ok, details) {
31
+ ctx.sendTo(ws, Object.assign({ type: "session_handoff_result", ok: ok }, details || {}));
32
+ }
33
+
34
+ function validate(ws, source, targetVendor) {
35
+ if (ctx.isMate) throw new Error("Session handoff is only available in projects.");
36
+ if (!source) throw new Error("No active session was found.");
37
+ if (ctx.splitStore && ctx.splitStore.groupForMember(source.localId)) {
38
+ throw new Error("Open the session by itself before continuing it in another agent.");
39
+ }
40
+ if (source.isProcessing || source._queryStarting) {
41
+ throw new Error("Wait for the current response to finish before continuing in another agent.");
42
+ }
43
+ if (!hasUserContext(source)) throw new Error("This session does not have any conversation to continue.");
44
+ if (!targetVendor) throw new Error("Choose an agent for the handoff.");
45
+ if (!ctx.adapters[targetVendor] || (sm.installedVendors || []).indexOf(targetVendor) === -1) {
46
+ throw new Error("The selected coding agent is not installed.");
47
+ }
48
+ var linuxUser = ctx.getLinuxUserForSession(source);
49
+ var vendorInfo = yoke.getVendorInfo(targetVendor);
50
+ if (linuxUser && vendorInfo && vendorInfo.osUserIsolation === false) {
51
+ throw new Error((vendorInfo.displayName || targetVendor) + " is not available for OS-isolated users.");
52
+ }
53
+ var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
54
+ if ((source.ownerId || null) !== ownerId) throw new Error("Session access denied.");
55
+ return { linuxUser: linuxUser, vendorInfo: vendorInfo };
56
+ }
57
+
58
+ function quietRecord(session, entry) {
59
+ session.history.push(entry);
60
+ sm.appendToSessionFile(session, entry);
61
+ }
62
+
63
+ function handoff(ws, msg) {
64
+ var source = sm.sessions.get(ws._clayActiveSession) || null;
65
+ var targetVendor = typeof msg.targetVendor === "string" ? msg.targetVendor.trim() : "";
66
+ var targetModel = typeof msg.model === "string" ? msg.model.trim() : "";
67
+ var validation;
68
+ try {
69
+ validation = validate(ws, source, targetVendor);
70
+ } catch (err) {
71
+ sendResult(ws, false, { error: err.message || String(err) });
72
+ return;
73
+ }
74
+
75
+ var targetEffort = yoke.clampEffort(
76
+ targetVendor,
77
+ msg.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[targetVendor]) || sm.currentEffort || "medium"
78
+ ) || null;
79
+ var target = sm.createSessionRaw({
80
+ ownerId: source.ownerId || null,
81
+ sessionVisibility: source.sessionVisibility || "shared",
82
+ vendor: targetVendor,
83
+ model: targetModel || null,
84
+ effort: targetEffort,
85
+ });
86
+ var targetName = (yoke.getVendorInfo(targetVendor) || {}).displayName || targetVendor;
87
+ target.title = (source.title || "Continued session") + " · " + targetName;
88
+ target.handoff = {
89
+ sourceSessionId: source.localId,
90
+ sourceVendor: source.vendor || sm.defaultVendor || "claude",
91
+ createdAt: Date.now(),
92
+ mode: "context",
93
+ sourceHistoryIndex: source.history.length,
94
+ };
95
+
96
+ var sourceEntry = {
97
+ type: "handoff_created",
98
+ targetSessionId: target.localId,
99
+ targetVendor: targetVendor,
100
+ targetTitle: target.title,
101
+ _ts: Date.now(),
102
+ };
103
+ var targetEntry = {
104
+ type: "handoff_context",
105
+ sourceSessionId: source.localId,
106
+ sourceVendor: target.handoff.sourceVendor,
107
+ sourceTitle: source.title || "Untitled session",
108
+ targetVendor: targetVendor,
109
+ request: contextBuilder.latestUserRequest(source.history),
110
+ _ts: Date.now(),
111
+ };
112
+ quietRecord(source, sourceEntry);
113
+ quietRecord(target, targetEntry);
114
+
115
+ var prompt = contextBuilder.buildHandoffContext({
116
+ cwd: ctx.cwd,
117
+ source: source,
118
+ targetVendor: targetVendor,
119
+ sourceReadTool: (validation.vendorInfo || {}).sessionBoundTools !== false,
120
+ });
121
+ target.isProcessing = true;
122
+ target.lastActivity = Date.now();
123
+ target.sentToolResults = {};
124
+ sm.switchSession(target.localId, ws);
125
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
126
+ sendResult(ws, true, {
127
+ sourceSessionId: source.localId,
128
+ targetSessionId: target.localId,
129
+ targetVendor: targetVendor,
130
+ });
131
+
132
+ var sdk = ctx.getSdk();
133
+ function failStart(err) {
134
+ target.isProcessing = false;
135
+ sm.sendAndRecord(target, { type: "error", text: "Could not start the handoff session: " + (err.message || String(err)) });
136
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
137
+ sm.broadcastSessionList();
138
+ }
139
+ if (!sdk || typeof sdk.startQuery !== "function") {
140
+ failStart(new Error("SDK bridge is not ready."));
141
+ return;
142
+ }
143
+ target._queryStartTs = Date.now();
144
+ var startPromise;
145
+ try {
146
+ startPromise = sdk.startQuery(target, prompt, undefined, validation.linuxUser);
147
+ } catch (err) {
148
+ failStart(err);
149
+ return;
150
+ }
151
+ Promise.resolve(startPromise).catch(failStart);
152
+ }
153
+
154
+ function handleMessage(ws, msg) {
155
+ if (msg.type === "handoff_session_options") {
156
+ ctx.sendTo(ws, {
157
+ type: "handoff_session_options",
158
+ installedVendors: sm.installedVendors || [],
159
+ modelsByVendor: sm.modelsByVendor || {},
160
+ capabilitiesByVendor: sm.capabilitiesByVendor || {},
161
+ });
162
+ return true;
163
+ }
164
+ if (msg.type === "handoff_session") {
165
+ handoff(ws, msg);
166
+ return true;
167
+ }
168
+ return false;
169
+ }
170
+
171
+ function resolveSourceChain(boundSession) {
172
+ var chain = [];
173
+ var visited = new Set();
174
+ var sourceSessionId = boundSession.handoff.sourceSessionId;
175
+ var ownerId = boundSession.ownerId || null;
176
+ for (var depth = 0; depth < MAX_HANDOFF_CHAIN_DEPTH; depth++) {
177
+ var visitKey = String(sourceSessionId);
178
+ if (visited.has(visitKey)) break;
179
+ visited.add(visitKey);
180
+ var source = sm.sessions.get(sourceSessionId);
181
+ if (!source) return { error: "Source session was not found: " + sourceSessionId };
182
+ if ((source.ownerId || null) !== ownerId) return { error: "Source session owner does not match this session" };
183
+ chain.push(source);
184
+ if (!source.handoff || source.handoff.sourceSessionId === undefined || source.handoff.sourceSessionId === null) break;
185
+ sourceSessionId = source.handoff.sourceSessionId;
186
+ }
187
+ return { chain: chain };
188
+ }
189
+
190
+ function findSource(chain, requestedId) {
191
+ if (requestedId === undefined || requestedId === null || requestedId === "") return chain[0] || null;
192
+ for (var i = 0; i < chain.length; i++) {
193
+ if (String(chain[i].localId) === String(requestedId)) return chain[i];
194
+ }
195
+ return null;
196
+ }
197
+
198
+ function boundedInteger(value, fallback, min, max) {
199
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
200
+ return Math.min(max, Math.max(min, Math.floor(value)));
201
+ }
202
+
203
+ function formatHistory(source, args) {
204
+ var history = Array.isArray(source.history) ? source.history : [];
205
+ var limit = boundedInteger(args.limit, 30, 1, 100);
206
+ var offset = typeof args.offset === "number" && Number.isFinite(args.offset)
207
+ ? boundedInteger(args.offset, 0, 0, history.length)
208
+ : Math.max(0, history.length - limit);
209
+ var slice = history.slice(offset, offset + limit);
210
+ var end = offset + slice.length;
211
+ var out = [];
212
+ out.push("# " + (source.title || "Untitled session") + " — " + (source.vendor || "unknown") + "/" + source.localId);
213
+ out.push("Showing entries " + (slice.length ? offset + 1 : 0) + "-" + end + " of " + history.length + "\n");
214
+ for (var i = 0; i < slice.length; i++) {
215
+ var entry = slice[i];
216
+ if (!entry) continue;
217
+ var label;
218
+ var text = "";
219
+ if (entry.type === "user_message") {
220
+ label = "USER";
221
+ text = entry.text || "";
222
+ } else if (entry.type === "delta") {
223
+ label = "ASSISTANT";
224
+ text = entry.text || "";
225
+ } else if (entry.type === "tool_executing" || entry.type === "tool_result") {
226
+ label = "TOOL";
227
+ text = (entry.name || "") + (entry.input ? " " + JSON.stringify(entry.input).substring(0, 120) : "");
228
+ } else {
229
+ continue;
230
+ }
231
+ if (text.length > 800) text = text.substring(0, 800) + "...";
232
+ out.push("[" + label + "] " + text);
233
+ }
234
+ return out.join("\n");
235
+ }
236
+
237
+ function readHandoffSource(args, boundSession) {
238
+ if (!boundSession) return toolError("read_handoff_source requires a session-bound tool server");
239
+ if (!boundSession.handoff) return toolError("this session does not have a handoff source");
240
+ var resolved = resolveSourceChain(boundSession);
241
+ if (resolved.error) return toolError(resolved.error);
242
+ var source = findSource(resolved.chain, args.sourceSessionId);
243
+ if (!source) return toolError("sourceSessionId is not in this session's handoff chain");
244
+ return toolResult(formatHistory(source, args));
245
+ }
246
+
247
+ function getToolDefs(boundSession) {
248
+ if (ctx.isMate) return [];
249
+ if (boundSession && !boundSession.handoff) return [];
250
+ return sessionHandoffMcp.getToolDefs({
251
+ read: function (args) { return readHandoffSource(args, boundSession || null); },
252
+ });
253
+ }
254
+
255
+ function createMcpServer(adapter, boundSession) {
256
+ if (ctx.isMate || !adapter || typeof adapter.createToolServer !== "function") return null;
257
+ var tools = getToolDefs(boundSession || null);
258
+ if (tools.length === 0) return null;
259
+ return adapter.createToolServer({
260
+ name: "clay-handoff",
261
+ version: "1.0.0",
262
+ tools: tools,
263
+ });
264
+ }
265
+
266
+ function getSystemPrompt() {
267
+ return "";
268
+ }
269
+
270
+ return {
271
+ createMcpServer: createMcpServer,
272
+ getSystemPrompt: getSystemPrompt,
273
+ getToolDefs: getToolDefs,
274
+ handleMessage: handleMessage,
275
+ };
276
+ }
277
+
278
+ module.exports = {
279
+ attachSessionHandoff: attachSessionHandoff,
280
+ hasUserContext: hasUserContext,
281
+ };