clay-server 4.1.0-beta.4 → 4.1.0-beta.6

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 (91) hide show
  1. package/lib/autonomous-run-record.js +32 -0
  2. package/lib/autonomous-run-scheduler.js +92 -0
  3. package/lib/durable-scheduler-record.js +61 -0
  4. package/lib/durable-scheduler-store.js +185 -0
  5. package/lib/durable-scheduler.js +495 -0
  6. package/lib/loop-cron.js +47 -0
  7. package/lib/loop-durable-schedule.js +159 -0
  8. package/lib/loop-guidance.js +35 -0
  9. package/lib/loop-registry-occurrence.js +76 -0
  10. package/lib/project-autonomous-run.js +498 -0
  11. package/lib/project-connection.js +4 -1
  12. package/lib/project-full-access.js +86 -0
  13. package/lib/project-http.js +3 -1
  14. package/lib/project-loop-interview.js +163 -0
  15. package/lib/project-loop.js +150 -64
  16. package/lib/project-pair-autonomous-stop.js +16 -0
  17. package/lib/project-pair-owned-stop.js +18 -0
  18. package/lib/project-pair-task-control.js +14 -1
  19. package/lib/project-scheduled-messages.js +316 -0
  20. package/lib/project-scheduled-task-execution.js +236 -0
  21. package/lib/project-scheduled-tasks.js +483 -0
  22. package/lib/project-session-pair.js +16 -19
  23. package/lib/project-sessions.js +60 -28
  24. package/lib/project-user-message.js +24 -18
  25. package/lib/project.js +260 -52
  26. package/lib/public/app.js +60 -23
  27. package/lib/public/css/home-hub.css +15 -2
  28. package/lib/public/css/home-sidebar.css +50 -2
  29. package/lib/public/css/input.css +71 -0
  30. package/lib/public/css/mates.css +0 -1
  31. package/lib/public/css/pane.css +58 -7
  32. package/lib/public/css/rewind.css +11 -2
  33. package/lib/public/css/scheduled-tasks.css +123 -0
  34. package/lib/public/css/sticky-notes.css +24 -4
  35. package/lib/public/index.html +19 -134
  36. package/lib/public/modules/app-loop-ui.js +4 -8
  37. package/lib/public/modules/app-messages.js +22 -6
  38. package/lib/public/modules/app-projects.js +2 -0
  39. package/lib/public/modules/app-rate-limit.js +8 -5
  40. package/lib/public/modules/autonomous-run.js +285 -0
  41. package/lib/public/modules/home-sidebar.js +16 -8
  42. package/lib/public/modules/home-surface.js +5 -0
  43. package/lib/public/modules/input.js +5 -4
  44. package/lib/public/modules/loop-interview.js +151 -0
  45. package/lib/public/modules/project-logs.js +2 -2
  46. package/lib/public/modules/scheduled-message-state.js +46 -0
  47. package/lib/public/modules/scheduled-task-edit-state.js +7 -0
  48. package/lib/public/modules/scheduled-task-engine.js +98 -0
  49. package/lib/public/modules/scheduled-task-form.js +122 -0
  50. package/lib/public/modules/scheduled-task-recurrence.js +46 -0
  51. package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
  52. package/lib/public/modules/scheduled-tasks.js +468 -0
  53. package/lib/public/modules/scheduler-config.js +6 -52
  54. package/lib/public/modules/scheduler.js +9 -21
  55. package/lib/public/modules/sidebar.js +0 -11
  56. package/lib/public/modules/split-view.js +2 -0
  57. package/lib/public/modules/sticky-notes-browser.js +1 -1
  58. package/lib/public/modules/sticky-notes.js +117 -4
  59. package/lib/public/modules/tool-palette-order.js +2 -3
  60. package/lib/public/modules/tools.js +5 -2
  61. package/lib/public/style.css +1 -0
  62. package/lib/record-revision.js +11 -0
  63. package/lib/schedule-rule.js +151 -0
  64. package/lib/schedule-validation.js +124 -0
  65. package/lib/scheduled-run-state.js +87 -0
  66. package/lib/scheduled-task-records.js +127 -0
  67. package/lib/scheduler.js +179 -196
  68. package/lib/sdk-bridge.js +123 -11
  69. package/lib/sdk-message-processor.js +3 -0
  70. package/lib/server-durable-scheduler.js +26 -0
  71. package/lib/server.js +22 -0
  72. package/lib/session-pair-factory.js +42 -4
  73. package/lib/session-pair-query-start.js +22 -0
  74. package/lib/session-query-tool-bridge.js +52 -0
  75. package/lib/session-split-groups.js +35 -1
  76. package/lib/sessions.js +51 -3
  77. package/lib/users-home-surface-preferences.js +2 -1
  78. package/lib/users-scheduled-task-preferences.js +44 -0
  79. package/lib/users.js +6 -0
  80. package/lib/ws-schema.js +25 -0
  81. package/lib/yoke/adapters/antigravity.js +1 -0
  82. package/lib/yoke/adapters/claude-worker.js +7 -4
  83. package/lib/yoke/adapters/claude.js +31 -1
  84. package/lib/yoke/adapters/codex.js +3 -0
  85. package/lib/yoke/claude-user-input.js +2 -0
  86. package/lib/yoke/index.js +2 -0
  87. package/lib/yoke/mcp-bridge-server.js +7 -1
  88. package/lib/yoke/session-tool-transport.js +53 -0
  89. package/lib/yoke/user-input.js +21 -7
  90. package/package.json +1 -1
  91. package/lib/public/modules/app-loop-wizard.js +0 -439
@@ -12,6 +12,27 @@ function vendorSupportsTui(vendor) {
12
12
  return !!(info && info.sessionModes.indexOf("tui") !== -1);
13
13
  }
14
14
 
15
+ function catalogHasModel(models, value) {
16
+ for (var i = 0; i < (models || []).length; i++) {
17
+ var model = models[i];
18
+ if (typeof model === "string" && model === value) return true;
19
+ if (model && (model.value === value || model.id === value || model.resolvedModel === value)) return true;
20
+ }
21
+ return false;
22
+ }
23
+
24
+ function catalogSupportsEffort(models, modelValue, effort, vendorLevels) {
25
+ if (!effort) return true;
26
+ for (var i = 0; i < (models || []).length; i++) {
27
+ var model = models[i];
28
+ var value = typeof model === "string" ? model : model && (model.value || model.id || model.resolvedModel);
29
+ if (value !== modelValue) continue;
30
+ if (model && Array.isArray(model.supportedEffortLevels) && model.supportedEffortLevels.length) return model.supportedEffortLevels.indexOf(effort) !== -1;
31
+ break;
32
+ }
33
+ return (vendorLevels || []).indexOf(effort) !== -1;
34
+ }
35
+
15
36
  // Format a user's answer to an ask_user_questions card as a plain user
16
37
  // message so the MCP path can feed it back to the agent on the next turn.
17
38
  // The agent sees its own question text alongside the selected answer(s),
@@ -98,6 +119,8 @@ function attachSessions(ctx) {
98
119
  var onProcessingChanged = ctx.onProcessingChanged;
99
120
  var broadcastPresence = ctx.broadcastPresence;
100
121
  var adapter = ctx.adapter;
122
+ var fullAccessService = ctx.fullAccessService;
123
+ var autonomousRun = ctx.autonomousRun;
101
124
 
102
125
  function ownsSearchSession(ws, session) {
103
126
  if (!session) return false;
@@ -118,6 +141,7 @@ function attachSessions(ctx) {
118
141
  catch (e) { return false; }
119
142
  }
120
143
  delete session.pendingAskUser[toolId];
144
+ if (autonomousRun) autonomousRun.onUserAnswer(session);
121
145
  sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId, answers: answers });
122
146
  if (pending.mode === "yoke" && typeof pending.respond === "function") {
123
147
  pending.respond(answers);
@@ -507,6 +531,34 @@ function attachSessions(ctx) {
507
531
  }
508
532
 
509
533
  if (msg.type === "new_session") {
534
+ if (msg.scheduleInterview === true) {
535
+ var sourceSession = getSessionForWs(ws);
536
+ var interviewVendor = typeof msg.vendor === "string" ? msg.vendor : "";
537
+ var installed = sm.installedVendors || [];
538
+ var knownModels = sm.modelsByVendor && sm.modelsByVendor[interviewVendor] || [];
539
+ var requestedModel = typeof msg.model === "string" ? msg.model : "";
540
+ var requestedEffort = typeof msg.effort === "string" ? msg.effort : "";
541
+ var vendorInfo = yoke.getVendorInfo(interviewVendor);
542
+ var engineError = "";
543
+ var sourceMatches = !!sourceSession && Number(msg.sourceSessionId) === Number(sourceSession.localId);
544
+ if (!sourceMatches || msg.projectSlug !== slug || !ctx.canCreateScheduledTaskInterview || !ctx.canCreateScheduledTaskInterview(ws, sourceSession)) engineError = "Scheduled Tasks is unavailable in the current project session.";
545
+ else if (!interviewVendor || installed.indexOf(interviewVendor) === -1 || !vendorInfo || yoke.sessionTools.capability(interviewVendor).kind === "none") engineError = "The selected interview engine is no longer available for assisted scheduling.";
546
+ else if (!knownModels.length) engineError = "Interview models are still loading or unavailable for this engine.";
547
+ else if (requestedModel && !catalogHasModel(knownModels, requestedModel)) engineError = "The selected interview model is no longer available.";
548
+ else if (!catalogSupportsEffort(knownModels, requestedModel, requestedEffort, vendorInfo.effortLevels)) engineError = "The selected interview effort is unavailable for this engine.";
549
+ if (engineError) {
550
+ if (msg.requestId) sendTo(ws, { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: false, error: engineError });
551
+ return true;
552
+ }
553
+ var preferenceUserId = ws._clayUser ? ws._clayUser.id : "default";
554
+ var savedPreference;
555
+ try { savedPreference = usersModule.setScheduledTaskInterviewEngine(preferenceUserId, { vendor: interviewVendor, model: requestedModel, effort: requestedEffort }); }
556
+ catch (preferenceError) { savedPreference = { error: preferenceError.message || String(preferenceError) }; }
557
+ if (!savedPreference || !savedPreference.ok) {
558
+ if (msg.requestId) sendTo(ws, { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: false, error: savedPreference && savedPreference.error || "The interview engine preference could not be saved." });
559
+ return true;
560
+ }
561
+ }
510
562
  // Opportunistic hygiene: drop blanks that sat untouched past the grace
511
563
  // period before deciding whether to reuse or create.
512
564
  sm.sweepBlankSessions();
@@ -514,6 +566,7 @@ function attachSessions(ctx) {
514
566
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
515
567
  sessionOpts.sessionVisibility = sessionVisibility.normalize(msg.sessionVisibility);
516
568
  if (msg.vendor) sessionOpts.vendor = msg.vendor;
569
+ if (msg.scheduleInterview === true && msg.model) sessionOpts.model = msg.model;
517
570
  // Mode resolution: vendors without a TUI session mode are always GUI.
518
571
  // TUI-capable sessions honor the explicit msg.mode if provided, otherwise
519
572
  // fall back to the user's claudeOpenMode preference. This is what
@@ -583,10 +636,9 @@ function attachSessions(ctx) {
583
636
  sm.switchSession(newSess.localId, ws);
584
637
  } else {
585
638
  var newSessionVendor = sessionOpts.vendor || sm.defaultVendor || "claude";
586
- sessionOpts.effort = yoke.clampEffort(
587
- newSessionVendor,
588
- (sm.currentEffortByVendor && sm.currentEffortByVendor[newSessionVendor]) || sm.currentEffort || "medium"
589
- ) || null;
639
+ sessionOpts.effort = msg.scheduleInterview === true && msg.effort
640
+ ? msg.effort
641
+ : yoke.clampEffort(newSessionVendor, (sm.currentEffortByVendor && sm.currentEffortByVendor[newSessionVendor]) || sm.currentEffort || "medium") || null;
590
642
  // Reuse an existing blank GUI session instead of stacking another
591
643
  // one. A vendor-less blank is acceptable: nothing has happened in it,
592
644
  // so stamping the requested vendor is equivalent to a fresh create.
@@ -606,6 +658,7 @@ function attachSessions(ctx) {
606
658
  }
607
659
  }
608
660
  ws._clayActiveSession = newSess.localId;
661
+ if (msg.requestId) sendTo(ws, { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: true, sessionId: newSess.localId });
609
662
  // Remember the vendor only when the client asked for one explicitly.
610
663
  // new_session without a vendor (mate sidebar, notification banner,
611
664
  // debate) falls through to the default adapter and must not clobber
@@ -1019,6 +1072,7 @@ function attachSessions(ctx) {
1019
1072
 
1020
1073
  if (msg.type === "stop") {
1021
1074
  var session = getSessionForWs(ws);
1075
+ if (autonomousRun && session && session.autonomousRun) autonomousRun.finish(session, "stopped", "Stopped by the user.");
1022
1076
  stopSession(session);
1023
1077
  return true;
1024
1078
  }
@@ -1150,34 +1204,12 @@ function attachSessions(ctx) {
1150
1204
  return true;
1151
1205
  }
1152
1206
 
1153
- var previousFullAccess = fullAccessSession.permissionMode === "bypassPermissions";
1154
- var previousPermissionMode = fullAccessSession.permissionMode || null;
1155
- var previousRestoreMode = fullAccessSession.permissionModeBeforeFullAccess || null;
1156
- if (fullAccessEnabled) {
1157
- if (!previousFullAccess) {
1158
- fullAccessSession.permissionModeBeforeFullAccess = fullAccessSession.permissionMode || sm.currentPermissionMode || "default";
1159
- }
1160
- fullAccessSession.permissionMode = "bypassPermissions";
1161
- } else {
1162
- fullAccessSession.permissionMode = fullAccessSession.permissionModeBeforeFullAccess || "default";
1163
- fullAccessSession.permissionModeBeforeFullAccess = null;
1164
- }
1165
-
1166
1207
  (async function () {
1167
1208
  try {
1168
- // Clay's common canUseTool callback owns permission skipping for
1169
- // every vendor. If an adapter supports native mode switching, keep
1170
- // it in a normal mode so Clay remains the single approval layer.
1171
- if (fullAccessSession.queryInstance && typeof fullAccessSession.queryInstance.setPermissionMode === "function") {
1172
- await fullAccessSession.queryInstance.setPermissionMode(fullAccessEnabled ? "default" : fullAccessSession.permissionMode);
1173
- }
1174
- sm.saveSessionFile(fullAccessSession);
1175
- sm.broadcastSessionList();
1176
- send({ type: "session_full_access_changed", id: fullAccessSession.localId, enabled: fullAccessEnabled, permissionMode: fullAccessSession.permissionMode });
1209
+ if (!fullAccessEnabled && autonomousRun) await autonomousRun.stopForPermissionChange(fullAccessSession);
1210
+ await fullAccessService.setEnabled(fullAccessSession, fullAccessEnabled);
1177
1211
  sendTo(ws, { type: "session_full_access_result", ok: true, id: fullAccessSession.localId, enabled: fullAccessEnabled });
1178
1212
  } catch (e) {
1179
- fullAccessSession.permissionMode = previousPermissionMode;
1180
- fullAccessSession.permissionModeBeforeFullAccess = previousRestoreMode;
1181
1213
  sendTo(ws, { type: "session_full_access_result", ok: false, id: fullAccessSession.localId, error: "Failed to change Skip Permissions: " + (e.message || e) });
1182
1214
  }
1183
1215
  })();
@@ -70,6 +70,7 @@ function attachUserMessage(ctx) {
70
70
 
71
71
  var scheduleMessage = ctx.scheduleMessage;
72
72
  var cancelScheduledMessage = ctx.cancelScheduledMessage;
73
+ var sendScheduledMessageNow = ctx.sendScheduledMessageNow;
73
74
 
74
75
  var loadContextSources = ctx.loadContextSources;
75
76
  var saveContextSources = ctx.saveContextSources;
@@ -281,34 +282,21 @@ function attachUserMessage(ctx) {
281
282
  if (msg.type === "schedule_message") {
282
283
  var schedSession = getSessionForWs(ws);
283
284
  if (!schedSession || !msg.text || !msg.resetsAt) return true;
284
- scheduleMessage(schedSession, msg.text, msg.resetsAt);
285
+ scheduleMessage(schedSession, msg.text, msg.resetsAt, ws._clayUser || null, ws);
285
286
  return true;
286
287
  }
287
288
 
288
289
  if (msg.type === "cancel_scheduled_message") {
289
290
  var cancelSession = getSessionForWs(ws);
290
291
  if (!cancelSession) return true;
291
- cancelScheduledMessage(cancelSession);
292
+ cancelScheduledMessage(cancelSession, ws._clayUser || null, ws, { jobId: msg.jobId, revision: msg.revision });
292
293
  return true;
293
294
  }
294
295
 
295
296
  if (msg.type === "send_scheduled_now") {
296
297
  var nowSession = getSessionForWs(ws);
297
298
  if (!nowSession || !nowSession.scheduledMessage) return true;
298
- var schedText = nowSession.scheduledMessage.text;
299
- clearTimeout(nowSession.scheduledMessage.timer);
300
- nowSession.scheduledMessage = null;
301
- console.log("[project] Scheduled message sent immediately for session " + nowSession.localId);
302
- sm.sendAndRecord(nowSession, { type: "scheduled_message_sent" });
303
- var userMsg = { type: "user_message", text: schedText, _ts: Date.now() };
304
- nowSession.history.push(userMsg);
305
- sm.appendToSessionFile(nowSession, userMsg);
306
- sendToSession(nowSession.localId, userMsg);
307
- nowSession.isProcessing = true;
308
- onProcessingChanged();
309
- sendToSession(nowSession.localId, { type: "status", status: "processing" });
310
- sdk.startQuery(nowSession, schedText, null, ensureProjectAccessForSession(nowSession));
311
- sm.broadcastSessionList();
299
+ sendScheduledMessageNow(nowSession, ws._clayUser || null, ws, { jobId: msg.jobId, revision: msg.revision });
312
300
  return true;
313
301
  }
314
302
 
@@ -318,6 +306,11 @@ function attachUserMessage(ctx) {
318
306
  var session = getSessionForWs(ws);
319
307
  if (!session) return true;
320
308
 
309
+ if (session.loopInterviewHandoff && session.loopInterviewHandoff.state === "starting") {
310
+ sendTo(ws, { type: "error", text: "Wait for the approved Loop handoff to finish." });
311
+ return true;
312
+ }
313
+
321
314
  var deliveryReceipt = messageDelivery.inspect(ws, session, msg);
322
315
  if (deliveryReceipt.duplicate) {
323
316
  messageDelivery.acknowledge(ws, deliveryReceipt, hydrateImageRefs(deliveryReceipt.recordedMessage));
@@ -339,6 +332,19 @@ function attachUserMessage(ctx) {
339
332
  });
340
333
  return true;
341
334
  }
335
+ if (msg.autonomousRunToken) {
336
+ if (!ctx.autonomousRun || !ctx.autonomousRun.consume(session, msg)) {
337
+ sendTo(ws, { type: "error", text: "This Until complete submission is stale or no longer armed." });
338
+ return true;
339
+ }
340
+ } else if (session.autonomousRun && session.autonomousRun.state === "armed") {
341
+ sendTo(ws, { type: "error", text: "Until complete is still being armed. Wait for its acknowledgement before sending." });
342
+ return true;
343
+ } else if (ctx.autonomousRun) {
344
+ ctx.autonomousRun.onHumanMessage(session, msg);
345
+ }
346
+ var autonomousDeliveryMeta = session.autonomousRun && ["running", "reviewing", "waiting-worker", "waiting-user"].indexOf(session.autonomousRun.state) !== -1
347
+ ? { autonomousRunId: session.autonomousRun.id } : undefined;
342
348
  if (typeof ctx.beginHumanPairTurn === "function") ctx.beginHumanPairTurn(session);
343
349
  if (gitAttribution) gitAttribution.beginTurn(session);
344
350
 
@@ -552,7 +558,7 @@ function attachUserMessage(ctx) {
552
558
  if (!session.queryInstance && !session._queryStarting && (!session.worker || session.messageQueue !== "worker")) {
553
559
  // No active query (or worker idle between queries): start a new query
554
560
  startFreshQuery(finalText);
555
- } else if (!sdk.pushMessage(session, finalText, msg.images)) {
561
+ } else if (!sdk.pushMessage(session, finalText, msg.images, autonomousDeliveryMeta)) {
556
562
  // Stale state won the branch (previous stream died between the
557
563
  // turn boundary and its cleanup) but there is nothing to deliver
558
564
  // to. Start a fresh query with this text instead of dropping it,
@@ -560,7 +566,7 @@ function attachUserMessage(ctx) {
560
566
  // response until the user sent the message again.
561
567
  startFreshQuery(finalText);
562
568
  }
563
- } else if (!sdk.pushMessage(session, finalText, msg.images)) {
569
+ } else if (!sdk.pushMessage(session, finalText, msg.images, autonomousDeliveryMeta)) {
564
570
  // isProcessing was stuck true with no live query: same recovery.
565
571
  startFreshQuery(finalText);
566
572
  }