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.
Files changed (66) 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 +162 -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 +33 -77
  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 +110 -0
  48. package/lib/session-notes-mcp-server.js +1 -0
  49. package/lib/sessions.js +4 -0
  50. package/lib/ws-schema.js +9 -1
  51. package/lib/yoke/acp-agent-profiles.js +64 -0
  52. package/lib/yoke/adapters/acp.js +2 -1
  53. package/lib/yoke/adapters/claude.js +34 -0
  54. package/lib/yoke/adapters/codex.js +38 -3
  55. package/lib/yoke/adapters/copilot.js +7 -0
  56. package/lib/yoke/adapters/grok.js +7 -0
  57. package/lib/yoke/adapters/junie.js +7 -0
  58. package/lib/yoke/adapters/kimi.js +7 -0
  59. package/lib/yoke/adapters/kiro.js +4 -3
  60. package/lib/yoke/adapters/qwen.js +7 -0
  61. package/lib/yoke/codex-background-tasks.js +84 -0
  62. package/lib/yoke/index.js +43 -14
  63. package/lib/yoke/instructions.js +3 -0
  64. package/lib/yoke/interface.js +21 -0
  65. package/lib/yoke/vendor-registry.js +55 -0
  66. 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,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 = "A Worker task delegated through send_to_partner has finished.\n\n" +
102
- "Original task:\n" + token.message + "\n\n" +
103
- (failure ? "Worker error:\n" + failure : "Worker result:\n" + (response || "(No text response was recorded.)")) +
104
- "\n\nReview the result, verify it as needed, and continue the task.";
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
  });