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
@@ -0,0 +1,18 @@
1
+ function stopOwnedPair(session, roles, turnControl, taskControl, workerPermission, reason) {
2
+ if (!roles) return false;
3
+ turnControl.markHumanStop(session);
4
+ taskControl.cancelAll(roles.worker);
5
+ taskControl.markInterruption(roles.worker, "user", reason || "The human stopped this scheduled run.");
6
+ workerPermission.cancelForSession(roles.worker, reason || "The human stopped this scheduled run.");
7
+ var members = [roles.driver, roles.worker];
8
+ for (var i = 0; i < members.length; i++) {
9
+ members[i].taskStopRequested = true;
10
+ if (members[i].queryInstance && typeof members[i].queryInstance.close === "function") {
11
+ try { members[i].queryInstance.close(); } catch (e) {}
12
+ }
13
+ if (members[i].abortController) { try { members[i].abortController.abort(); } catch (e) {} }
14
+ }
15
+ return true;
16
+ }
17
+
18
+ module.exports = { stopOwnedPair: stopOwnedPair };
@@ -67,6 +67,7 @@ function attachPairTaskControl(ctx) {
67
67
  workerSessionId: worker.localId,
68
68
  driverSessionId: caller.localId,
69
69
  message: message,
70
+ autonomousRunId: caller.autonomousRun && caller.autonomousRun.id || null,
70
71
  createdAt: Date.now(),
71
72
  status: "running",
72
73
  };
@@ -118,6 +119,18 @@ function attachPairTaskControl(ctx) {
118
119
  return result({ status: "cancelled", taskId: task.taskId });
119
120
  } catch (err) { return result({ status: "rejected", taskId: args.taskId || null, reason: err.message || String(err) }); }
120
121
  }
122
+ function cancelAll(worker, autonomousRunId) {
123
+ var queue = queueFor(worker);
124
+ var cancelled = 0;
125
+ for (var i = 0; i < queue.length; i++) {
126
+ if (queue[i].status !== "queued" && queue[i].status !== "waiting_for_interrupt") continue;
127
+ if (autonomousRunId && queue[i].autonomousRunId !== autonomousRunId) continue;
128
+ queue[i].status = "cancelled";
129
+ queue[i].completedAt = Date.now();
130
+ cancelled++;
131
+ }
132
+ return cancelled;
133
+ }
121
134
  function markInterruption(worker, by, reason, targetTaskId) {
122
135
  var active = current(worker);
123
136
  worker._pairInterruption = {
@@ -258,7 +271,7 @@ function attachPairTaskControl(ctx) {
258
271
  function workerToolDefs(worker) {
259
272
  return [{ name: "report_partner_outcome", description: "Report structured, Worker-observed task outcome data. This does not claim Driver verification.", inputSchema: buildShape({ taskId: { type: "string" }, outcome: { type: "string" }, summary: { type: "string" }, changedFiles: { type: "array", items: { type: "string" } }, verifications: { type: "array", items: { type: "object" } }, unverified: { type: "array", items: { type: "string" } }, processes: { type: "array", items: { type: "string" } }, nextAction: { type: "string" } }, ["taskId", "outcome"]), handler: function (args) { return report(args || {}, worker); } }];
260
273
  }
261
- return { begin: begin, cancel: cancel, complete: complete, drain: drain, driverToolDefs: driverToolDefs, ensureSystemInterruption: ensureSystemInterruption, inspect: inspect, markInterruption: markInterruption, queue: queue, replaceTask: replaceTask, resumeTask: resumeTask, status: status, workerToolDefs: workerToolDefs };
274
+ return { begin: begin, cancel: cancel, cancelAll: cancelAll, complete: complete, drain: drain, driverToolDefs: driverToolDefs, ensureSystemInterruption: ensureSystemInterruption, inspect: inspect, markInterruption: markInterruption, queue: queue, replaceTask: replaceTask, resumeTask: resumeTask, status: status, workerToolDefs: workerToolDefs };
262
275
  }
263
276
 
264
277
  module.exports = { attachPairTaskControl: attachPairTaskControl };
@@ -0,0 +1,316 @@
1
+ var crypto = require("crypto");
2
+ var sessionProvenance = require("./session-provenance");
3
+
4
+ var LOCAL_OWNER = "local-user";
5
+
6
+ function attachProjectScheduledMessages(ctx) {
7
+ var scheduler = ctx.scheduler;
8
+ var slug = ctx.slug;
9
+ var sm = ctx.sm;
10
+ var sdk = ctx.sdk;
11
+ var users = ctx.usersModule;
12
+ var type = "project-chat-message:" + slug;
13
+ var makeRevision = ctx.makeRevision || function () { return crypto.randomUUID(); };
14
+ var now = ctx.now || Date.now;
15
+ var unregister = null;
16
+ var stopped = false;
17
+
18
+ function findSession(cliSessionId) {
19
+ var found = null;
20
+ sm.sessions.forEach(function (session) {
21
+ if (!found && session.cliSessionId === cliSessionId) found = session;
22
+ });
23
+ return found;
24
+ }
25
+
26
+ function latestJob(session) {
27
+ if (!scheduler || !session || !session.cliSessionId) return null;
28
+ var jobs = scheduler.listJobs();
29
+ for (var i = jobs.length - 1; i >= 0; i--) {
30
+ if (jobs[i].type === type && jobs[i].projectId === slug && jobs[i].target && jobs[i].target.id === session.cliSessionId) return jobs[i];
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function pendingJob(session) {
36
+ if (!scheduler || !session || !session.cliSessionId) return null;
37
+ if (session.scheduledMessage && session.scheduledMessage.jobId) {
38
+ var direct = scheduler.getJob(session.scheduledMessage.jobId);
39
+ if (direct && direct.state === "queued") return direct;
40
+ }
41
+ var jobs = scheduler.listJobs();
42
+ for (var i = jobs.length - 1; i >= 0; i--) {
43
+ if (jobs[i].type === type && jobs[i].projectId === slug && jobs[i].state === "queued" &&
44
+ jobs[i].target && jobs[i].target.id === session.cliSessionId) return jobs[i];
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function projection(job) {
50
+ if (!job || job.state !== "queued") return null;
51
+ return {
52
+ jobId: job.id,
53
+ revision: job.payload.revision,
54
+ text: job.payload.text,
55
+ resetsAt: job.runAt,
56
+ scheduledAt: job.payload.scheduledAt,
57
+ };
58
+ }
59
+
60
+ function sendState(session, targetWs, sendFn) {
61
+ var job = pendingJob(session);
62
+ session.scheduledMessage = projection(job);
63
+ var latest = latestJob(session);
64
+ var interrupted = !job && latest && latest.state === "interrupted" ? {
65
+ jobId: latest.id,
66
+ revision: latest.payload.revision,
67
+ text: latest.payload.text,
68
+ } : null;
69
+ var message = { type: "scheduled_message_state", scheduledMessage: session.scheduledMessage, interrupted: interrupted };
70
+ if (sendFn) sendFn(message);
71
+ else if (targetWs) ctx.sendTo(targetWs, message);
72
+ else ctx.sendToSession(session.localId, message);
73
+ return message;
74
+ }
75
+
76
+ function visibleError(session, targetWs, text) {
77
+ var message = { type: "error", text: text };
78
+ if (targetWs) ctx.sendTo(targetWs, message);
79
+ else if (session) ctx.sendToSession(session.localId, message);
80
+ }
81
+
82
+ function ownerFor(session) {
83
+ if (!users.isMultiUser()) return LOCAL_OWNER;
84
+ if (!session.ownerId) throw new Error("Scheduled messages require a session owner.");
85
+ if (!users.findUserById(session.ownerId)) throw new Error("The session owner no longer exists.");
86
+ return session.ownerId;
87
+ }
88
+
89
+ function authorize(session, actorId) {
90
+ if (!session || session.destroying) throw new Error("The session is no longer available.");
91
+ if (sm.sessions.get(session.localId) !== session) throw new Error("The session is no longer live.");
92
+ if (!session.cliSessionId) throw new Error("This session has no persistent provider identity yet.");
93
+ if (session.mode === "tui" || session.runtimeMode === "tui" || sessionProvenance.isWorker(session) ||
94
+ (ctx.isDriverOperatedSession && ctx.isDriverOperatedSession(session))) {
95
+ throw new Error("Scheduled messages are available only for ordinary project chat sessions.");
96
+ }
97
+ if (!users.isMultiUser()) return LOCAL_OWNER;
98
+ if (!actorId || !users.findUserById(actorId)) throw new Error("The scheduling user no longer exists.");
99
+ var access = ctx.getProjectAccess();
100
+ if (!ctx.canAccessProjectSlug(actorId, slug) || !users.canAccessSession(actorId, session, access)) {
101
+ throw new Error("Scheduled message access was revoked.");
102
+ }
103
+ var ownerId = ownerFor(session);
104
+ if (!ctx.canAccessProjectSlug(ownerId, slug) || !users.canAccessSession(ownerId, session, access)) {
105
+ throw new Error("The session owner no longer has access.");
106
+ }
107
+ return ownerId;
108
+ }
109
+
110
+ function buildSpec(session, text, resetsAt, actorId, revision) {
111
+ var timestamp = now();
112
+ var runAt = resetsAt <= timestamp ? timestamp + 5000 : resetsAt + 60000;
113
+ var ownerId = authorize(session, actorId);
114
+ return {
115
+ type: type,
116
+ idempotencyKey: "project-message:" + slug + ":" + session.cliSessionId + ":" + revision,
117
+ ownerId: ownerId,
118
+ projectId: slug,
119
+ target: { kind: "project-session", id: session.cliSessionId },
120
+ payload: {
121
+ text: text,
122
+ cliSessionId: session.cliSessionId,
123
+ actorId: actorId || LOCAL_OWNER,
124
+ sessionOwnerId: ownerId,
125
+ revision: revision,
126
+ requestedResetsAt: resetsAt,
127
+ scheduledAt: timestamp,
128
+ },
129
+ runAt: runAt,
130
+ };
131
+ }
132
+
133
+ function schedule(session, text, resetsAt, actorUser, targetWs) {
134
+ if (!scheduler || stopped) {
135
+ visibleError(session, targetWs, "Scheduled messages are temporarily unavailable.");
136
+ return null;
137
+ }
138
+ if (typeof text !== "string" || !text.trim() || !isFinite(Number(resetsAt))) {
139
+ visibleError(session, targetWs, "A valid scheduled message and reset time are required.");
140
+ return null;
141
+ }
142
+ try {
143
+ var actorId = users.isMultiUser() ? (actorUser && actorUser.id) : LOCAL_OWNER;
144
+ var revision = makeRevision();
145
+ var spec = buildSpec(session, text, Number(resetsAt), actorId, revision);
146
+ var current = pendingJob(session);
147
+ var job = current ? scheduler.replaceQueued(current.id, spec) : scheduler.enqueue(spec);
148
+ session.scheduledMessage = projection(job);
149
+ sm.sendAndRecord(session, Object.assign({ type: "scheduled_message_queued" }, session.scheduledMessage));
150
+ return job;
151
+ } catch (error) {
152
+ if (session) session.rateLimitAutoContinuePending = false;
153
+ visibleError(session, targetWs, "Could not schedule the message: " + error.message);
154
+ return null;
155
+ }
156
+ }
157
+
158
+ function requireCorrelation(job, correlation, targetWs) {
159
+ if (!targetWs) return;
160
+ if (!correlation || correlation.jobId !== job.id || correlation.revision !== job.payload.revision) {
161
+ throw new Error("The scheduled-message control is stale.");
162
+ }
163
+ }
164
+
165
+ function cancel(session, actorUser, targetWs, reason, correlation) {
166
+ try {
167
+ authorize(session, users.isMultiUser() ? (actorUser && actorUser.id) : LOCAL_OWNER);
168
+ var job = pendingJob(session);
169
+ if (!job) return false;
170
+ requireCorrelation(job, correlation, targetWs);
171
+ scheduler.cancel(job.id, reason || "cancelled_by_user");
172
+ session.scheduledMessage = null;
173
+ session.rateLimitAutoContinuePending = false;
174
+ sm.sendAndRecord(session, { type: "scheduled_message_cancelled", jobId: job.id, revision: job.payload.revision });
175
+ return true;
176
+ } catch (error) {
177
+ visibleError(session, targetWs, "Could not cancel the scheduled message: " + error.message);
178
+ return false;
179
+ }
180
+ }
181
+
182
+ function sendNow(session, actorUser, targetWs, correlation) {
183
+ try {
184
+ authorize(session, users.isMultiUser() ? (actorUser && actorUser.id) : LOCAL_OWNER);
185
+ var job = pendingJob(session);
186
+ if (!job) return false;
187
+ requireCorrelation(job, correlation, targetWs);
188
+ var updated = scheduler.replaceQueued(job.id, {
189
+ runAt: now(),
190
+ idempotencyKey: job.idempotencyKey + ":now",
191
+ });
192
+ session.scheduledMessage = projection(updated);
193
+ scheduler.tick();
194
+ return true;
195
+ } catch (error) {
196
+ visibleError(session, targetWs, "Could not send the scheduled message: " + error.message);
197
+ return false;
198
+ }
199
+ }
200
+
201
+ function canRun(job) {
202
+ var session = findSession(job.target && job.target.id);
203
+ if (!session) return true;
204
+ return !session.isProcessing && !session._queryStarting;
205
+ }
206
+
207
+ function failJob(session, job, error, processingOwner) {
208
+ var ownsProcessing = session && processingOwner && session._scheduledMessageProcessing === processingOwner &&
209
+ session._queryGeneration === processingOwner.queryGeneration &&
210
+ session.queryInstance === processingOwner.queryInstance && !session._queryStarting;
211
+ if (ownsProcessing && session.isProcessing) {
212
+ session.isProcessing = false;
213
+ ctx.onProcessingChanged();
214
+ ctx.sendToSession(session.localId, { type: "status", status: "idle" });
215
+ sm.broadcastSessionList();
216
+ }
217
+ if (session && session._scheduledMessageProcessing === processingOwner) delete session._scheduledMessageProcessing;
218
+ if (session && session.scheduledMessage && session.scheduledMessage.jobId === job.id) {
219
+ session.scheduledMessage = null;
220
+ session.rateLimitAutoContinuePending = false;
221
+ sm.sendAndRecord(session, { type: "scheduled_message_cancelled", jobId: job.id, revision: job.payload.revision, reason: error.message });
222
+ }
223
+ if (session) sm.sendAndRecord(session, { type: "error", text: "Scheduled message failed: " + error.message });
224
+ }
225
+
226
+ async function dispatch(job, execution) {
227
+ var session = findSession(job.target.id);
228
+ var processingOwner = null;
229
+ try {
230
+ if (!session) throw new Error("The scheduled session was deleted.");
231
+ var ownerId = authorize(session, job.payload.actorId);
232
+ if (ownerId !== job.ownerId || ownerId !== job.payload.sessionOwnerId) throw new Error("The session owner changed.");
233
+ if (ctx.osUsers) {
234
+ var owner = users.findUserById(job.ownerId);
235
+ if (!owner || !owner.linuxUser) throw new Error("The session owner has no OS identity.");
236
+ }
237
+ var linuxUser = ctx.ensureProjectAccessForSession(session);
238
+ if (ctx.osUsers && !linuxUser) throw new Error("The session owner OS identity is unavailable.");
239
+ execution.recordMetadata({ projectSlug: slug, cliSessionId: session.cliSessionId, revision: job.payload.revision });
240
+ execution.recordReceipt({ state: "dispatching", projectSlug: slug, cliSessionId: session.cliSessionId, revision: job.payload.revision, recordedAt: now() });
241
+
242
+ var userMessage = {
243
+ type: "user_message",
244
+ text: job.payload.text,
245
+ _ts: now(),
246
+ scheduledJobId: job.id,
247
+ scheduledRevision: job.payload.revision,
248
+ };
249
+ session.history.push(userMessage);
250
+ sm.appendToSessionFile(session, userMessage);
251
+ ctx.sendToSession(session.localId, userMessage);
252
+ processingOwner = {};
253
+ session._scheduledMessageProcessing = processingOwner;
254
+ session.isProcessing = true;
255
+ ctx.onProcessingChanged();
256
+ ctx.sendToSession(session.localId, { type: "status", status: "processing" });
257
+ var queryPromise = sdk.startQuery(session, job.payload.text, null, linuxUser);
258
+ processingOwner.queryGeneration = session._queryGeneration;
259
+ processingOwner.queryInstance = session.queryInstance;
260
+ if (session.scheduledMessage && session.scheduledMessage.jobId === job.id) {
261
+ session.scheduledMessage = null;
262
+ session.rateLimitAutoContinuePending = false;
263
+ }
264
+ sm.sendAndRecord(session, { type: "scheduled_message_sent", jobId: job.id, revision: job.payload.revision });
265
+ sm.broadcastSessionList();
266
+ await Promise.resolve(queryPromise);
267
+ if (session._scheduledMessageProcessing === processingOwner) delete session._scheduledMessageProcessing;
268
+ return { dispatched: true, cliSessionId: session.cliSessionId, revision: job.payload.revision };
269
+ } catch (error) {
270
+ failJob(session, job, error, processingOwner);
271
+ throw error;
272
+ }
273
+ }
274
+
275
+ function rehydrate() {
276
+ sm.sessions.forEach(function (session) { session.scheduledMessage = projection(pendingJob(session)); });
277
+ }
278
+
279
+ function deleted(localId, session) {
280
+ var job = pendingJob(session);
281
+ if (!job) return;
282
+ try { scheduler.cancel(job.id, "session_deleted"); } catch (error) {}
283
+ session.scheduledMessage = null;
284
+ }
285
+
286
+ function notify() {
287
+ if (!scheduler || stopped) return;
288
+ try { scheduler.tick(); } catch (error) {}
289
+ }
290
+
291
+ function shutdown() {
292
+ stopped = true;
293
+ if (unregister) unregister();
294
+ unregister = null;
295
+ }
296
+
297
+ if (scheduler) {
298
+ unregister = scheduler.registerHandler(type, dispatch, { canRun: canRun });
299
+ rehydrate();
300
+ sm.addOnSessionDeleted(deleted);
301
+ sm.addOnSessionViewed(function (session, targetWs, sendFn) { sendState(session, targetWs, sendFn); });
302
+ }
303
+
304
+ return {
305
+ schedule: schedule,
306
+ cancel: cancel,
307
+ sendNow: sendNow,
308
+ sendState: sendState,
309
+ rehydrate: rehydrate,
310
+ notify: notify,
311
+ shutdown: shutdown,
312
+ handlerType: type,
313
+ };
314
+ }
315
+
316
+ module.exports = { attachProjectScheduledMessages: attachProjectScheduledMessages, LOCAL_OWNER: LOCAL_OWNER };
@@ -0,0 +1,236 @@
1
+ var crypto = require("crypto");
2
+ var fs = require("fs");
3
+ var path = require("path");
4
+ var sessionProvenance = require("./session-provenance");
5
+ var sessionToolTransport = require("./yoke/session-tool-transport");
6
+ var ID_PATTERN = /^[A-Za-z0-9_-]{1,200}$/;
7
+
8
+ function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
9
+ function clean(value, max) { var text = typeof value === "string" ? value.trim() : ""; return text.slice(0, max); }
10
+
11
+ function validRuntime(runtime) {
12
+ return !!(runtime && runtime.driver && runtime.worker && runtime.driver.vendor && runtime.worker.vendor);
13
+ }
14
+
15
+ function attachScheduledTaskExecution(ctx) {
16
+ var registry = ctx.registry;
17
+ var sm = ctx.sm;
18
+ var pair = ctx.sessionPair;
19
+
20
+ function promptFor(record) {
21
+ var filesId = record.linkedTaskId || record.id;
22
+ if (!ID_PATTERN.test(record.id || "") || !ID_PATTERN.test(filesId || "")) throw new Error("Task storage identity is invalid.");
23
+ var promptPath = path.join(ctx.cwd, ".claude", "loops", filesId, "PROMPT.md");
24
+ return fs.readFileSync(promptPath, "utf8").trim();
25
+ }
26
+
27
+ function runnable(record) {
28
+ if (!record || (record.createdViaScheduledTasks !== true && record.source !== "schedule")) return { ok: false, legacy: true };
29
+ if (!validRuntime(record.execution)) return { ok: false, error: "Choose Driver and Split Worker runtimes before this task can run." };
30
+ if (!ctx.authorize(record)) return { ok: false, error: "The scheduled task owner is no longer authorized for this project." };
31
+ if (!sessionToolTransport.capability(record.execution.driver.vendor).supported) return { ok: false, error: "The Driver runtime cannot receive scheduled outcome tools." };
32
+ if (!sessionToolTransport.capability(record.execution.worker.vendor).supported) return { ok: false, error: "The Split Worker runtime cannot receive pair tools." };
33
+ try {
34
+ pair.preflightRuntime(record.execution.driver, null, true);
35
+ pair.preflightRuntime(record.execution.worker, null, true);
36
+ } catch (error) { return { ok: false, error: error.message || String(error) }; }
37
+ return { ok: true };
38
+ }
39
+
40
+ function rollbackPair(created) {
41
+ if (!created) return;
42
+ if (pair.removeOwnedPair && pair.removeOwnedPair(created.driver.ownerId || null, created)) return;
43
+ try { sm.deleteSessionQuiet(created.worker.localId); } catch (e) {}
44
+ try { sm.deleteSessionQuiet(created.driver.localId); } catch (e) {}
45
+ }
46
+
47
+ function finish(recordId, runId, outcome, reason, drain) {
48
+ var record = registry.getById(recordId);
49
+ if (!record || !record.activeRun || record.activeRun.runId !== runId) return false;
50
+ var active = record.activeRun;
51
+ var driver = sm.sessions.get(active.driverSessionId);
52
+ var worker = sm.sessions.get(active.workerSessionId);
53
+ var saved = registry.completeRun(recordId, runId, {
54
+ outcome: outcome, reason: clean(reason, 1000) || null,
55
+ driverProviderSessionId: driver && driver.cliSessionId || active.driverProviderSessionId || null,
56
+ workerProviderSessionId: worker && worker.cliSessionId || active.workerProviderSessionId || null,
57
+ });
58
+ if (!saved) return false;
59
+ if (driver && driver.scheduledTaskRun && driver.scheduledTaskRun.runId === runId) driver.scheduledTaskRun.status = outcome;
60
+ if (worker && worker.scheduledTaskRun && worker.scheduledTaskRun.runId === runId) worker.scheduledTaskRun.status = outcome;
61
+ if (driver) sm.saveSessionFile(driver);
62
+ if (worker) sm.saveSessionFile(worker);
63
+ var current = registry.getById(recordId);
64
+ if (drain && current && current.pendingRun && !current.activeRun) trigger(current, "queued");
65
+ return true;
66
+ }
67
+
68
+ function failStart(recordId, runId, created, error, preDispatch) {
69
+ if (preDispatch) rollbackPair(created);
70
+ else if (created) {
71
+ created.driver.scheduledTaskRun.revoked = true;
72
+ created.worker.scheduledTaskRun.revoked = true;
73
+ pair.stopOwnedPair(created.driver, "The scheduled run failed after dispatch.");
74
+ }
75
+ finish(recordId, runId, "failed", error && (error.message || String(error)) || "Scheduled execution could not start.", false);
76
+ }
77
+
78
+ function newRun(record, source) {
79
+ return { runId: "srun_" + crypto.randomUUID(), scheduleId: record.id, source: source || "schedule", status: "starting", startedAt: Date.now() };
80
+ }
81
+
82
+ function prepare(record, source) {
83
+ var check = runnable(record);
84
+ if (!check.ok) return { ok: false, legacy: check.legacy, error: check.error || null };
85
+ if (record.activeRun) {
86
+ if (record.skipIfRunning !== false) return { ok: true, skipped: true, skippedRun: { runId: "skip_" + crypto.randomUUID(), scheduleId: record.id, source: source || "schedule", outcome: "skipped-busy", reason: "A prior occurrence was still active.", startedAt: Date.now(), finishedAt: Date.now() } };
87
+ return { ok: true, queued: true, pendingRun: record.pendingRun || { scheduleId: record.id, source: source || "schedule", queuedAt: Date.now(), dueAt: record.nextRunAt } };
88
+ }
89
+ return { ok: true, activeRun: newRun(record, source) };
90
+ }
91
+
92
+ function trigger(record, source, prepared) {
93
+ var check = runnable(record);
94
+ if (!check.ok) return { ok: false, legacy: check.legacy, error: check.error || null };
95
+ var run = prepared && prepared.activeRun;
96
+ if (run) {
97
+ if (!record.activeRun || record.activeRun.runId !== run.runId) return { ok: false, error: "The scheduled run reservation is no longer current." };
98
+ } else {
99
+ if (record.activeRun) return { ok: false, busy: true, skipped: record.skipIfRunning !== false, error: "This scheduled task already has an active run." };
100
+ run = newRun(record, source);
101
+ if (!registry.beginRun(record.id, run)) return { ok: false, error: "The scheduled run could not be reserved." };
102
+ }
103
+ var runId = run.runId;
104
+ var created = null;
105
+ var dispatched = false;
106
+ try {
107
+ var instructions = promptFor(record);
108
+ if (!instructions) throw new Error("Task instructions are empty.");
109
+ created = pair.createOwnedPair(record.ownerId || null, {
110
+ name: record.name || "Scheduled task",
111
+ driver: record.execution.driver,
112
+ worker: record.execution.worker,
113
+ });
114
+ var runState = {
115
+ status: "running", groupId: created.group.id,
116
+ driverSessionId: created.driver.localId, workerSessionId: created.worker.localId,
117
+ driverOriginId: sessionProvenance.ensureOrigin(created.driver), workerOriginId: sessionProvenance.ensureOrigin(created.worker),
118
+ };
119
+ if (!registry.updateRun(record.id, runId, runState)) throw new Error("The scheduled pair could not be recorded before execution.");
120
+ created.driver.scheduledTaskRun = Object.assign({}, runState, { runId: runId, scheduleId: record.id, role: "driver" });
121
+ created.worker.scheduledTaskRun = Object.assign({}, runState, { runId: runId, scheduleId: record.id, role: "worker" });
122
+ sm.saveSessionFile(created.driver); sm.saveSessionFile(created.worker);
123
+ var liveRecord = registry.getById(record.id);
124
+ var liveCheck = runnable(liveRecord);
125
+ if (!liveCheck.ok || !liveRecord.activeRun || liveRecord.activeRun.runId !== runId || liveRecord.ownerId !== created.driver.ownerId || liveRecord.ownerId !== created.worker.ownerId) {
126
+ throw new Error(liveCheck.error || "The scheduled task owner or run changed before execution.");
127
+ }
128
+ var linuxUser = ctx.getLinuxUserForSession(created.driver);
129
+ if (ctx.requiresLinuxUser && ctx.requiresLinuxUser() && !linuxUser) throw new Error("The scheduled task owner no longer has a valid OS identity.");
130
+ var prompt = "Run the scheduled task below with your visible Split Worker. Use the existing pair tools to delegate meaningful work, review the Worker's result, and decide whether the scheduled occurrence completed. Before ending, call report_scheduled_task_outcome with this exact run id. Report needs-input when ordinary permission or user input prevents completion.\n\nRun id: " + runId + "\nTask: " + (record.name || "Scheduled task") + "\n\n" + instructions;
131
+ sm.sendAndRecord(created.driver, { type: "user_message", text: prompt, scheduledTaskRunId: runId, scheduledTaskId: record.id });
132
+ created.driver.isProcessing = true;
133
+ ctx.onProcessingChanged();
134
+ sm.sendToSession(created.driver, { type: "status", status: "processing" });
135
+ var query = ctx.getSdk().startQuery(created.driver, prompt, undefined, linuxUser);
136
+ dispatched = true;
137
+ var generation = Number(created.driver._sdkQueryGeneration || 0);
138
+ created.driver.scheduledTaskRun.initialQueryGeneration = generation;
139
+ if (!registry.updateRun(record.id, runId, { initialQueryGeneration: generation })) throw new Error("The scheduled query identity could not be stored before acknowledgement.");
140
+ sm.saveSessionFile(created.driver);
141
+ Promise.resolve(query).catch(function (error) {
142
+ if (!created.driver.scheduledTaskRun || created.driver.scheduledTaskRun.runId !== runId || Number(created.driver._sdkQueryGeneration || 0) !== generation) return;
143
+ created.driver.isProcessing = false;
144
+ failStart(record.id, runId, created, error, false);
145
+ ctx.onProcessingChanged();
146
+ });
147
+ sm.broadcastSessionList();
148
+ return { ok: true, runId: runId, groupId: created.group.id, driverSessionId: created.driver.localId, workerSessionId: created.worker.localId };
149
+ } catch (error) {
150
+ failStart(record.id, runId, created, error, !dispatched);
151
+ return { ok: false, error: error.message || String(error) };
152
+ }
153
+ }
154
+
155
+ function exactRun(session, skipAuthorization) {
156
+ var marker = session && session.scheduledTaskRun;
157
+ if (!marker || marker.revoked || marker.role !== "driver" || sm.sessions.get(session.localId) !== session) return null;
158
+ var record = registry.getById(marker.scheduleId);
159
+ if (!record || !record.activeRun || record.activeRun.runId !== marker.runId || record.activeRun.driverOriginId !== session.sessionOriginId || (!skipAuthorization && !ctx.authorize(record))) return null;
160
+ var roles = pair.rolesFor(session);
161
+ if (!roles || roles.driver !== session || roles.group.id !== record.activeRun.groupId || roles.worker.sessionOriginId !== record.activeRun.workerOriginId || roles.worker.ownerId !== record.ownerId) return null;
162
+ return { marker: marker, record: record, roles: roles };
163
+ }
164
+
165
+ function revokeAndStop(current, reason) {
166
+ current.roles.driver.scheduledTaskRun.revoked = true;
167
+ if (current.roles.worker.scheduledTaskRun) current.roles.worker.scheduledTaskRun.revoked = true;
168
+ return pair.stopOwnedPair(current.roles.driver, reason);
169
+ }
170
+
171
+ function getToolDefs(session) {
172
+ var exact = exactRun(session);
173
+ if (!exact) return [];
174
+ var runId = exact.marker.runId;
175
+ var queryGeneration = Number(session._sdkQueryGeneration || 0);
176
+ return [{
177
+ name: "report_scheduled_task_outcome", queryBound: true,
178
+ description: "Record the authoritative outcome of this exact scheduled run. Completed is accepted only after all Worker and queued pair work is idle. Use needs-input when ordinary permission or user input is required.",
179
+ inputSchema: { type: "object", properties: { runId: { type: "string" }, outcome: { type: "string", enum: ["completed", "failed", "needs-input"] }, summary: { type: "string" } }, required: ["runId", "outcome"], additionalProperties: false },
180
+ handler: function (args) {
181
+ args = args && typeof args === "object" ? args : {};
182
+ var keys = Object.keys(args);
183
+ if (keys.some(function (key) { return ["runId", "outcome", "summary"].indexOf(key) === -1; }) || typeof args.runId !== "string" || ["completed", "failed", "needs-input"].indexOf(args.outcome) === -1 || (args.summary !== undefined && typeof args.summary !== "string")) return toolResult({ status: "rejected", reason: "The scheduled outcome arguments are invalid." });
184
+ var current = exactRun(session);
185
+ if (!current || args.runId !== runId || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return toolResult({ status: "rejected", reason: "This outcome tool belongs to a different query or finished scheduled run." });
186
+ var worker = current.roles.worker;
187
+ var queued = worker && Array.isArray(worker._pairFollowups) && worker._pairFollowups.some(function (item) { return item.status === "queued" || item.status === "waiting_for_interrupt" || item.status === "starting"; });
188
+ if (args.outcome === "completed" && worker && (worker.isProcessing || worker._queryStarting || worker._pairDelegation || queued)) return toolResult({ status: "rejected", reason: "The Split Worker or queued pair work is still active." });
189
+ if (args.outcome === "completed" && (!worker || !worker._lastPairOutcome || worker._lastPairOutcome.completedAt < current.record.activeRun.startedAt)) return toolResult({ status: "rejected", reason: "The Driver must delegate this occurrence to its Split Worker before reporting completion." });
190
+ if (args.outcome === "needs-input") {
191
+ if (!registry.updateRun(current.record.id, runId, { status: "needs-input", summary: clean(args.summary, 1000) || null, driverProviderSessionId: session.cliSessionId || null, workerProviderSessionId: worker && worker.cliSessionId || null })) return toolResult({ status: "rejected", reason: "The needs-input state could not be stored." });
192
+ session.scheduledTaskRun.status = "needs-input";
193
+ return toolResult({ status: "recorded", runId: runId, outcome: "needs-input" });
194
+ }
195
+ if (args.outcome === "failed") revokeAndStop(current, clean(args.summary, 1000) || "The scheduled Driver reported that this run failed.");
196
+ if (!finish(current.record.id, runId, args.outcome, args.summary, true)) return toolResult({ status: "rejected", reason: "The outcome could not be stored." });
197
+ return toolResult({ status: "recorded", runId: runId, outcome: args.outcome });
198
+ },
199
+ }];
200
+ }
201
+
202
+ function onTurnDone(session) {
203
+ var current = exactRun(session);
204
+ if (!current) return false;
205
+ if (session._lastTurnInterrupted || session.taskStopRequested) return finish(current.record.id, current.marker.runId, "interrupted", "The scheduled Driver was stopped.", false);
206
+ var worker = current.roles.worker;
207
+ if (worker.isProcessing || worker._queryStarting || worker._pairDelegation) return registry.updateRun(current.record.id, current.marker.runId, { status: "waiting-worker" });
208
+ if (current.record.activeRun.status === "running" || current.record.activeRun.status === "reviewing") return registry.updateRun(current.record.id, current.marker.runId, { status: "needs-input", reason: "The Driver turn ended without reporting a final scheduled outcome." });
209
+ return false;
210
+ }
211
+
212
+ function onPartnerResult(driver) {
213
+ var current = exactRun(driver, true);
214
+ if (current && !ctx.authorize(current.record)) return stopForSession(driver, "The scheduled task owner is no longer authorized for this project.");
215
+ return current ? registry.updateRun(current.record.id, current.marker.runId, { status: "reviewing" }) : false;
216
+ }
217
+
218
+ function stopForSession(session, reason) {
219
+ var roles = pair.rolesFor(session);
220
+ var driver = roles && roles.driver || session;
221
+ var current = exactRun(driver, true);
222
+ if (!current) return false;
223
+ revokeAndStop(current, reason || "The scheduled run was stopped.");
224
+ return finish(current.record.id, current.marker.runId, "interrupted", reason || "The scheduled run was stopped.", false);
225
+ }
226
+
227
+ function stopRecord(record, reason) {
228
+ if (!record || !record.activeRun) return false;
229
+ var driver = sm.sessions.get(record.activeRun.driverSessionId);
230
+ return driver ? stopForSession(driver, reason) : finish(record.id, record.activeRun.runId, "interrupted", reason, false);
231
+ }
232
+
233
+ return { getToolDefs: getToolDefs, onPartnerResult: onPartnerResult, onTurnDone: onTurnDone, prepare: prepare, runnable: runnable, stopForSession: stopForSession, stopRecord: stopRecord, trigger: trigger };
234
+ }
235
+
236
+ module.exports = { attachScheduledTaskExecution: attachScheduledTaskExecution, validRuntime: validRuntime };