clay-server 2.47.0-beta.3 → 2.47.0-beta.4

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.
@@ -43,7 +43,6 @@ var CLAY_MANAGED_ALLOW = [
43
43
  "mcp__clay-browser__browser_watch_tab",
44
44
  "mcp__clay-browser__browser_unwatch_tab",
45
45
  "mcp__clay-debate__propose_debate",
46
- "mcp__clay-datastore__*",
47
46
  "mcp__clay-history__*",
48
47
  // Email: read-side only. Send / reply / mark_read still prompt.
49
48
  "mcp__clay-email__clay_read_email",
@@ -0,0 +1,366 @@
1
+ var yoke = require("./yoke");
2
+ var sessionSpawnMcp = require("./session-spawn-mcp-server");
3
+ var cliSessions = require("./cli-sessions");
4
+ var os = require("os");
5
+ var osUsers = require("./os-users");
6
+
7
+ var MAX_SESSIONS_PER_CALL = 10;
8
+ var MAX_CHILDREN_PER_PARENT = 20;
9
+ var SPAWN_CONCURRENCY = 3;
10
+
11
+ function parseBatch(raw) {
12
+ var parsed;
13
+ try {
14
+ parsed = JSON.parse(raw);
15
+ } catch (e) {
16
+ throw new Error("sessions must be a valid JSON array");
17
+ }
18
+ if (!Array.isArray(parsed)) {
19
+ throw new Error("sessions must be a valid JSON array");
20
+ }
21
+ if (parsed.length < 1 || parsed.length > MAX_SESSIONS_PER_CALL) {
22
+ throw new Error("sessions must contain between 1 and 10 entries");
23
+ }
24
+ var result = [];
25
+ for (var i = 0; i < parsed.length; i++) {
26
+ var entry = parsed[i];
27
+ if (!entry || typeof entry.prompt !== "string" || !entry.prompt.trim()) {
28
+ throw new Error("session " + (i + 1) + " must include a non-empty prompt");
29
+ }
30
+ var title = typeof entry.title === "string" ? entry.title.trim() : "";
31
+ result.push({
32
+ title: title || "Spawned task " + (i + 1),
33
+ prompt: entry.prompt.trim(),
34
+ });
35
+ }
36
+ return result;
37
+ }
38
+
39
+ function getSessionsArray(sessions) {
40
+ if (!sessions) return [];
41
+ if (typeof sessions.values === "function") return Array.from(sessions.values());
42
+ return sessions;
43
+ }
44
+
45
+ function assertSpawnAllowed(parent, sessions, requestedCount) {
46
+ if (!parent) throw new Error("no active parent session");
47
+ if (parent.spawn) throw new Error("spawned sessions cannot spawn further sessions");
48
+ var all = getSessionsArray(sessions);
49
+ var childCount = 0;
50
+ for (var i = 0; i < all.length; i++) {
51
+ if (all[i].spawn && all[i].spawn.parentId === parent.localId) childCount++;
52
+ }
53
+ if (childCount + requestedCount > MAX_CHILDREN_PER_PARENT) {
54
+ throw new Error("a parent session cannot have more than 20 children");
55
+ }
56
+ return childCount;
57
+ }
58
+
59
+ function validateVendor(vendor, adapters, linuxUser, getVendorInfo) {
60
+ if (!vendor || !adapters || !adapters[vendor]) {
61
+ throw new Error("vendor is not available: " + (vendor || "unknown"));
62
+ }
63
+ var lookup = getVendorInfo || yoke.getVendorInfo;
64
+ var info = lookup(vendor);
65
+ if (linuxUser && info && info.osUserIsolation === false) {
66
+ throw new Error((info.displayName || vendor) + " is not available for OS-isolated users");
67
+ }
68
+ return vendor;
69
+ }
70
+
71
+ function createSpawnQueue(concurrency) {
72
+ var limit = concurrency || SPAWN_CONCURRENCY;
73
+ var pending = [];
74
+ var running = [];
75
+
76
+ function startTask(task) {
77
+ task.state = "running";
78
+ running.push(task);
79
+ var finished = false;
80
+ function complete(err) {
81
+ if (finished) return;
82
+ finished = true;
83
+ var index = running.indexOf(task);
84
+ if (index !== -1) running.splice(index, 1);
85
+ task.state = err ? "error" : "done";
86
+ task.error = err || null;
87
+ pump();
88
+ }
89
+ try {
90
+ task.start(complete);
91
+ } catch (e) {
92
+ complete(e);
93
+ }
94
+ }
95
+
96
+ function pump() {
97
+ while (running.length < limit && pending.length > 0) {
98
+ var task = pending.shift();
99
+ startTask(task);
100
+ }
101
+ }
102
+
103
+ function add(tasks) {
104
+ for (var i = 0; i < tasks.length; i++) {
105
+ tasks[i].state = "queued";
106
+ pending.push(tasks[i]);
107
+ }
108
+ pump();
109
+ var queued = 0;
110
+ var active = 0;
111
+ for (var j = 0; j < tasks.length; j++) {
112
+ if (tasks[j].state === "queued") queued++;
113
+ if (tasks[j].state === "running") active++;
114
+ }
115
+ return { queued: queued, running: active };
116
+ }
117
+
118
+ return {
119
+ add: add,
120
+ get pendingCount() { return pending.length; },
121
+ get runningCount() { return running.length; },
122
+ };
123
+ }
124
+
125
+ function hasSessionError(session) {
126
+ var history = session.history || [];
127
+ var recent = history.slice(-3);
128
+ for (var i = 0; i < recent.length; i++) {
129
+ if (recent[i].type === "error" || (recent[i].type === "done" && recent[i].code === 1)) return true;
130
+ }
131
+ return false;
132
+ }
133
+
134
+ function toolResult(value) {
135
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
136
+ }
137
+
138
+ function toolError(err) {
139
+ return Promise.resolve({
140
+ content: [{ type: "text", text: "Error: " + (err.message || err) }],
141
+ isError: true,
142
+ });
143
+ }
144
+
145
+ function rebuildMessageUUIDs(history) {
146
+ var messageUUIDs = [];
147
+ for (var i = 0; i < history.length; i++) {
148
+ if (history[i].type === "message_uuid") {
149
+ messageUUIDs.push({
150
+ uuid: history[i].uuid,
151
+ type: history[i].messageType,
152
+ historyIndex: i,
153
+ });
154
+ }
155
+ }
156
+ return messageUUIDs;
157
+ }
158
+
159
+ function attachSessionSpawn(ctx) {
160
+ var sm = ctx.sm;
161
+ var queue = createSpawnQueue(SPAWN_CONCURRENCY);
162
+ var adapters = ctx.adapters || {};
163
+ var readCliSessionHistory = ctx.readCliSessionHistory || cliSessions.readCliSessionHistory;
164
+
165
+ function resolveSessionHome(session) {
166
+ var linuxUser = ctx.getLinuxUserForSession(session);
167
+ if (linuxUser) {
168
+ try {
169
+ var info = osUsers.resolveOsUserInfo(linuxUser);
170
+ if (info && info.home) return info.home;
171
+ } catch (e) {}
172
+ }
173
+ return os.homedir();
174
+ }
175
+
176
+ function validateFork(parent, vendor, explicitVendor) {
177
+ if (!parent.cliSessionId) {
178
+ throw new Error("forkFromCurrent requires the calling session to have at least one completed turn");
179
+ }
180
+ var capabilities = sm.capabilitiesByVendor && sm.capabilitiesByVendor[vendor];
181
+ if (!capabilities || capabilities.fork !== true) {
182
+ throw new Error("forkFromCurrent is not supported by vendor: " + vendor);
183
+ }
184
+ var parentVendor = parent.vendor || sm.defaultVendor;
185
+ if (explicitVendor && explicitVendor !== parentVendor) {
186
+ throw new Error("forkFromCurrent children must use the parent's vendor");
187
+ }
188
+ }
189
+
190
+ function applyForkToTask(task, forkResult, history) {
191
+ task.session.cliSessionId = forkResult.sessionId;
192
+ task.session.history = history.slice();
193
+ task.session.messageUUIDs = rebuildMessageUUIDs(task.session.history);
194
+ sm.saveSessionFile(task.session);
195
+ }
196
+
197
+ function spawnOne(parent, spec, index, batchId, vendor, linuxUser) {
198
+ var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
199
+ var session = create.call(sm, {
200
+ ownerId: parent.ownerId || null,
201
+ sessionVisibility: parent.sessionVisibility || "shared",
202
+ vendor: vendor,
203
+ });
204
+ session.spawn = { parentId: parent.localId, index: index, batchId: batchId };
205
+ session.title = spec.title;
206
+ sm.saveSessionFile(session);
207
+
208
+ return {
209
+ session: session,
210
+ state: "created",
211
+ start: function (done) {
212
+ function finish(err) {
213
+ delete session.onQueryComplete;
214
+ delete session.singleTurn;
215
+ done(err);
216
+ }
217
+ var userMessage = { type: "user_message", text: spec.prompt };
218
+ session.history.push(userMessage);
219
+ sm.appendToSessionFile(session, userMessage);
220
+ session.isProcessing = true;
221
+ session.lastActivity = Date.now();
222
+ session.sentToolResults = {};
223
+ session.singleTurn = true;
224
+ session.onQueryComplete = function () {
225
+ finish(hasSessionError(session) ? new Error("child session failed") : null);
226
+ };
227
+ var sdk = ctx.getSdk();
228
+ if (!sdk || typeof sdk.startQuery !== "function") {
229
+ session.isProcessing = false;
230
+ finish(new Error("SDK bridge is not ready"));
231
+ return;
232
+ }
233
+ Promise.resolve(sdk.startQuery(session, spec.prompt, undefined, linuxUser)).then(function () {
234
+ if (!session.isProcessing && !session.queryInstance) {
235
+ finish(new Error("child session failed to start"));
236
+ }
237
+ }).catch(function (err) {
238
+ session.isProcessing = false;
239
+ session.history.push({ type: "error", text: err.message || String(err) });
240
+ finish(err);
241
+ });
242
+ },
243
+ };
244
+ }
245
+
246
+ async function spawn(args, caller) {
247
+ try {
248
+ // The caller is bound per query in getLocalMcpServers (project.js), so
249
+ // a child session resolving itself here is what makes the depth guard
250
+ // sound. Never fall back to sm.getActiveSession(): that is the
251
+ // project-global "last viewed" session and can belong to someone else.
252
+ var parent = caller;
253
+ if (!parent) throw new Error("spawn_sessions requires a session-bound tool server");
254
+ var specs = parseBatch(args.sessions);
255
+ assertSpawnAllowed(parent, sm.sessions, specs.length);
256
+ var explicitVendor = args.vendor && args.vendor.trim();
257
+ var vendor = explicitVendor || parent.vendor || sm.defaultVendor;
258
+ var linuxUser = ctx.getLinuxUserForSession(parent);
259
+ validateVendor(vendor, adapters, linuxUser);
260
+ var forkFromCurrent = args.forkFromCurrent === true;
261
+ if (forkFromCurrent) validateFork(parent, vendor, explicitVendor);
262
+ var batchId = "sp_" + Date.now().toString(36);
263
+ var tasks = [];
264
+ var spawned = [];
265
+ var failed = null;
266
+ var sdk = ctx.getSdk();
267
+ var lastMessage = parent.messageUUIDs && parent.messageUUIDs.length > 0
268
+ ? parent.messageUUIDs[parent.messageUUIDs.length - 1]
269
+ : null;
270
+ // Claude treats an omitted upToMessageId as a full-session fork;
271
+ // Codex forks the whole thread and ignores the UUID option.
272
+ var lastUuid = lastMessage ? lastMessage.uuid : undefined;
273
+ for (var i = 0; i < specs.length; i++) {
274
+ var forkResult = null;
275
+ var forkHistory = null;
276
+ if (forkFromCurrent) {
277
+ try {
278
+ if (!sdk || typeof sdk.forkSession !== "function") throw new Error("SDK bridge is not ready");
279
+ forkResult = await sdk.forkSession(parent, lastUuid);
280
+ if (!forkResult || !forkResult.sessionId) throw new Error("Fork returned no session id");
281
+ if (forkResult.useLocalHistory) {
282
+ forkHistory = parent.history.slice();
283
+ } else {
284
+ forkHistory = await readCliSessionHistory(
285
+ resolveSessionHome(parent), ctx.cwd, forkResult.sessionId
286
+ );
287
+ }
288
+ } catch (e) {
289
+ failed = { index: i, error: e.message || String(e) };
290
+ break;
291
+ }
292
+ }
293
+ var task = spawnOne(parent, specs[i], i, batchId, vendor, linuxUser);
294
+ if (forkResult) applyForkToTask(task, forkResult, forkHistory || []);
295
+ tasks.push(task);
296
+ spawned.push({ localId: task.session.localId, title: task.session.title });
297
+ }
298
+ var counts = queue.add(tasks);
299
+ if (tasks.length > 0) sm.broadcastSessionList();
300
+ var result = { spawned: spawned, queued: counts.queued, running: counts.running };
301
+ if (failed) result.failed = failed;
302
+ return toolResult(result);
303
+ } catch (e) {
304
+ return toolError(e);
305
+ }
306
+ }
307
+
308
+ function check(args, caller) {
309
+ try {
310
+ var parent = caller;
311
+ if (!parent) throw new Error("check_spawned_sessions requires a session-bound tool server");
312
+ var parentOnly = args.parentOnly !== false;
313
+ var all = getSessionsArray(sm.sessions);
314
+ var statuses = [];
315
+ for (var i = 0; i < all.length; i++) {
316
+ var session = all[i];
317
+ if (!session.spawn) continue;
318
+ if (parentOnly && session.spawn.parentId !== parent.localId) continue;
319
+ statuses.push({
320
+ localId: session.localId,
321
+ title: session.title || "New Session",
322
+ status: session.isProcessing ? "running" : (hasSessionError(session) ? "error" : "done"),
323
+ turnCount: session.turnCount || 0,
324
+ lastActivity: session.lastActivity || session.createdAt || 0,
325
+ });
326
+ }
327
+ return toolResult(statuses);
328
+ } catch (e) {
329
+ return toolError(e);
330
+ }
331
+ }
332
+
333
+ // boundSession: the session whose query this tool server instance is
334
+ // mounted into. Omitted for the static instance kept in the project's
335
+ // mcpServers map, which exists only so tool descriptors can be listed;
336
+ // its handlers fail closed if anything ever routes a call to them.
337
+ function createMcpServer(adapter, boundSession) {
338
+ if (ctx.isMate || !adapter || typeof adapter.createToolServer !== "function") return null;
339
+ return adapter.createToolServer({
340
+ name: "clay-sessions",
341
+ version: "1.0.0",
342
+ tools: sessionSpawnMcp.getToolDefs({
343
+ spawn: function (args) { return spawn(args, boundSession || null); },
344
+ check: function (args) { return check(args, boundSession || null); },
345
+ }),
346
+ });
347
+ }
348
+
349
+ return {
350
+ createMcpServer: createMcpServer,
351
+ spawnOne: spawnOne,
352
+ };
353
+ }
354
+
355
+ module.exports = {
356
+ MAX_SESSIONS_PER_CALL: MAX_SESSIONS_PER_CALL,
357
+ MAX_CHILDREN_PER_PARENT: MAX_CHILDREN_PER_PARENT,
358
+ SPAWN_CONCURRENCY: SPAWN_CONCURRENCY,
359
+ parseBatch: parseBatch,
360
+ assertSpawnAllowed: assertSpawnAllowed,
361
+ validateVendor: validateVendor,
362
+ createSpawnQueue: createSpawnQueue,
363
+ hasSessionError: hasSessionError,
364
+ rebuildMessageUUIDs: rebuildMessageUUIDs,
365
+ attachSessionSpawn: attachSessionSpawn,
366
+ };
package/lib/project.js CHANGED
@@ -28,9 +28,9 @@ var { attachSessions } = require("./project-sessions");
28
28
  var { attachUserMessage } = require("./project-user-message");
29
29
  var { attachConnection } = require("./project-connection");
30
30
  var { attachMcp } = require("./project-mcp");
31
- var { attachMateDatastore } = require("./project-mate-datastore");
32
31
  var { createLocalMcp } = require("./mcp-local");
33
32
  var { attachEmail: attachEmailModule } = require("./project-email");
33
+ var { attachSessionSpawn } = require("./project-session-spawn");
34
34
  // project-notifications is attached globally in server.js, passed via opts.notificationsModule
35
35
 
36
36
  // --- Context Sources persistence ---
@@ -476,23 +476,34 @@ function createProjectContext(opts) {
476
476
  },
477
477
  });
478
478
 
479
- // --- Mate datastore (Mate projects only) ---
480
- var _mateDatastore = attachMateDatastore({
479
+ // The SDK bridge is created after local MCP servers. Session spawning uses
480
+ // a getter so tool handlers see the initialized bridge when they run.
481
+ var sdk = null;
482
+ var _sessionSpawn = attachSessionSpawn({
481
483
  cwd: cwd,
482
- slug: slug,
483
- isMate: isMate,
484
+ sm: sm,
485
+ getSdk: function () { return sdk; },
484
486
  send: send,
485
- sendTo: sendTo,
486
- clients: clients,
487
- getSessionForWs: getSessionForWs,
487
+ isMate: isMate,
488
488
  usersModule: usersModule,
489
- getProjectOwnerId: function () { return projectOwnerId; },
489
+ adapters: adapters,
490
+ getLinuxUserForSession: getLinuxUserForSession,
490
491
  });
491
492
 
492
493
  // --- MCP tool servers (created via YOKE adapter) ---
493
494
  var mcpServers = (function () {
494
495
  var servers = {};
495
496
 
497
+ // Agent-driven sibling session fan-out (main projects only).
498
+ if (!isMate) {
499
+ try {
500
+ var sessionSpawnMcpConfig = _sessionSpawn.createMcpServer(adapter);
501
+ if (sessionSpawnMcpConfig) servers[sessionSpawnMcpConfig.name || "clay-sessions"] = sessionSpawnMcpConfig;
502
+ } catch (e) {
503
+ console.error("[project] Failed to create session spawn MCP server:", e.message);
504
+ }
505
+ }
506
+
496
507
  // Debate MCP server (available to both mates and main project)
497
508
  try {
498
509
  var debateMcp = require("./debate-mcp-server");
@@ -643,15 +654,6 @@ function createProjectContext(opts) {
643
654
  console.error("[project] Failed to create email MCP server:", e.message);
644
655
  }
645
656
 
646
- if (isMate) {
647
- try {
648
- var datastoreMcp = _mateDatastore.createMcpServer();
649
- if (datastoreMcp) servers[datastoreMcp.name || "clay-datastore"] = datastoreMcp;
650
- } catch (e) {
651
- console.error("[project] Failed to create datastore MCP server:", e.message);
652
- }
653
- }
654
-
655
657
  return Object.keys(servers).length > 0 ? servers : undefined;
656
658
  })();
657
659
 
@@ -663,7 +665,12 @@ function createProjectContext(opts) {
663
665
  //
664
666
  // clay-browser -> only when the Chrome extension is connected
665
667
  // clay-email -> only when the user has an account or server SMTP
666
- function getLocalMcpServers() {
668
+ //
669
+ // forSession (optional): the session whose query these servers are mounted
670
+ // into. clay-sessions must know its caller (depth guard + child ownership),
671
+ // so it is re-instantiated bound to that session; the static instance in
672
+ // mcpServers only serves descriptor listing and fails closed on calls.
673
+ function getLocalMcpServers(forSession) {
667
674
  if (!mcpServers) return undefined;
668
675
  var extWs = browserState._extensionWs;
669
676
  var extConnected = !!(extWs && extWs.readyState === 1);
@@ -675,6 +682,15 @@ function createProjectContext(opts) {
675
682
  var name = keys[i];
676
683
  if (name === "clay-browser" && !extConnected) continue;
677
684
  if (name === "clay-email" && !emailAvailable) continue;
685
+ if (name === "clay-sessions" && forSession) {
686
+ try {
687
+ var boundSpawn = _sessionSpawn.createMcpServer(adapter, forSession);
688
+ if (boundSpawn) { filtered[name] = boundSpawn; hasAny = true; }
689
+ } catch (e) {
690
+ console.error("[project] Failed to bind session spawn MCP server:", e.message);
691
+ }
692
+ continue;
693
+ }
678
694
  filtered[name] = mcpServers[name];
679
695
  hasAny = true;
680
696
  }
@@ -682,7 +698,7 @@ function createProjectContext(opts) {
682
698
  }
683
699
 
684
700
  // --- SDK bridge ---
685
- var sdk = createSDKBridge({
701
+ sdk = createSDKBridge({
686
702
  cwd: cwd,
687
703
  slug: slug,
688
704
  sessionManager: sm,
@@ -1028,9 +1044,6 @@ function createProjectContext(opts) {
1028
1044
  // --- MCP bridge (remote MCP servers via extension) ---
1029
1045
  if (_mcp.handleMcpMessage(ws, msg)) return;
1030
1046
 
1031
- // --- Mate datastore ---
1032
- if (_mateDatastore.handleMateDatastoreMessage(ws, msg)) return;
1033
-
1034
1047
  // --- Knowledge file management (delegated to project-knowledge.js) ---
1035
1048
  if (_knowledge.handleKnowledgeMessage(ws, msg)) return;
1036
1049
 
@@ -1491,9 +1504,6 @@ function createProjectContext(opts) {
1491
1504
  function destroy() {
1492
1505
  _loop.stopTimer();
1493
1506
  _email.destroy();
1494
- if (_mateDatastore && typeof _mateDatastore.closeAllDatastores === "function") {
1495
- try { _mateDatastore.closeAllDatastores(); } catch (e) {}
1496
- }
1497
1507
  stopFileWatch();
1498
1508
  stopAllDirWatches();
1499
1509
  // Abort all active sessions and clean up mention sessions
package/lib/public/app.js CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  openMobileSheet, setMobileSheetMateData, refreshMobileChatSheet
19
19
  } from './modules/sidebar-mobile.js';
20
20
  import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile, handleMateSearchResults } from './modules/mate-sidebar.js';
21
- import { initMateDatastoreUI } from './modules/mate-datastore-ui.js';
22
21
  import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
23
22
  import { initMateMemory, renderMemoryList, hideMemory } from './modules/mate-memory.js';
24
23
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton, onRewindComplete, onRewindError } from './modules/rewind.js';
@@ -416,7 +415,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
416
415
  initSidebar(sidebarCtx);
417
416
  var wsGetter = function () { return _getWsRef(); };
418
417
  initMateSidebar(wsGetter);
419
- initMateDatastoreUI(wsGetter);
420
418
  initMateKnowledge(wsGetter);
421
419
  initMateMemory(wsGetter, { onShow: function () { hideKnowledge(); hideNotes(); } });
422
420
  initMateWizard(