clay-server 2.7.1 → 2.7.2

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.
package/lib/scheduler.js DELETED
@@ -1,362 +0,0 @@
1
- /**
2
- * Loop Registry — unified storage for all Ralph Loops (one-off + scheduled).
3
- *
4
- * Stores loop records in ~/.clay/loops/{encodedCwd}.jsonl
5
- * Each record represents a job defined via clay-ralph (PROMPT.md + JUDGE.md).
6
- * Records with a `cron` field are checked every 30s and auto-triggered.
7
- * Records without cron are one-off (standard Ralph Loop behavior).
8
- */
9
-
10
- var fs = require("fs");
11
- var path = require("path");
12
- var { CONFIG_DIR } = require("./config");
13
- var { encodeCwd } = require("./utils");
14
-
15
- // --- Cron parser (5-field: minute hour day-of-month month day-of-week) ---
16
-
17
- function parseCronField(field, min, max) {
18
- var values = [];
19
- var parts = field.split(",");
20
- for (var i = 0; i < parts.length; i++) {
21
- var part = parts[i].trim();
22
-
23
- // wildcard with step: */N
24
- if (part.indexOf("/") !== -1) {
25
- var slashParts = part.split("/");
26
- var step = parseInt(slashParts[1], 10);
27
- var rangeStr = slashParts[0];
28
- var rangeMin = min;
29
- var rangeMax = max;
30
- if (rangeStr !== "*") {
31
- var rp = rangeStr.split("-");
32
- rangeMin = parseInt(rp[0], 10);
33
- rangeMax = rp.length > 1 ? parseInt(rp[1], 10) : rangeMin;
34
- }
35
- for (var v = rangeMin; v <= rangeMax; v += step) {
36
- values.push(v);
37
- }
38
- continue;
39
- }
40
-
41
- // wildcard
42
- if (part === "*") {
43
- for (var v = min; v <= max; v++) {
44
- values.push(v);
45
- }
46
- continue;
47
- }
48
-
49
- // range: N-M
50
- if (part.indexOf("-") !== -1) {
51
- var rangeParts = part.split("-");
52
- var from = parseInt(rangeParts[0], 10);
53
- var to = parseInt(rangeParts[1], 10);
54
- for (var v = from; v <= to; v++) {
55
- values.push(v);
56
- }
57
- continue;
58
- }
59
-
60
- // single value
61
- values.push(parseInt(part, 10));
62
- }
63
- return values;
64
- }
65
-
66
- function parseCron(expr) {
67
- var fields = expr.trim().split(/\s+/);
68
- if (fields.length !== 5) return null;
69
- return {
70
- minutes: parseCronField(fields[0], 0, 59),
71
- hours: parseCronField(fields[1], 0, 23),
72
- daysOfMonth: parseCronField(fields[2], 1, 31),
73
- months: parseCronField(fields[3], 1, 12),
74
- daysOfWeek: parseCronField(fields[4], 0, 6),
75
- };
76
- }
77
-
78
- function cronMatches(parsed, date) {
79
- var minute = date.getMinutes();
80
- var hour = date.getHours();
81
- var dayOfMonth = date.getDate();
82
- var month = date.getMonth() + 1;
83
- var dayOfWeek = date.getDay();
84
-
85
- return (
86
- parsed.minutes.indexOf(minute) !== -1 &&
87
- parsed.hours.indexOf(hour) !== -1 &&
88
- parsed.daysOfMonth.indexOf(dayOfMonth) !== -1 &&
89
- parsed.months.indexOf(month) !== -1 &&
90
- parsed.daysOfWeek.indexOf(dayOfWeek) !== -1
91
- );
92
- }
93
-
94
- /**
95
- * Calculate next run time from a cron expression after a given date.
96
- * Brute-force: check each minute for up to 366 days.
97
- */
98
- function nextRunTime(cronExpr, after) {
99
- var parsed = parseCron(cronExpr);
100
- if (!parsed) return null;
101
-
102
- var d = new Date(after || Date.now());
103
- d.setSeconds(0, 0);
104
- d.setMinutes(d.getMinutes() + 1);
105
-
106
- var limit = 366 * 24 * 60;
107
- for (var i = 0; i < limit; i++) {
108
- if (cronMatches(parsed, d)) {
109
- return d.getTime();
110
- }
111
- d.setMinutes(d.getMinutes() + 1);
112
- }
113
- return null;
114
- }
115
-
116
- // --- Loop Registry factory ---
117
-
118
- function createLoopRegistry(opts) {
119
- var cwd = opts.cwd;
120
- var onTrigger = opts.onTrigger;
121
- var onChange = opts.onChange;
122
-
123
- var encoded = encodeCwd(cwd);
124
- var registryDir = path.join(CONFIG_DIR, "loops");
125
- var registryPath = path.join(registryDir, encoded + ".jsonl");
126
-
127
- var records = [];
128
- var timerId = null;
129
- var CHECK_INTERVAL = 30 * 1000;
130
- var lastTriggeredMinute = {};
131
-
132
- // --- Persistence (JSONL) ---
133
-
134
- function load() {
135
- try {
136
- var raw = fs.readFileSync(registryPath, "utf8");
137
- var lines = raw.trim().split("\n");
138
- records = [];
139
- for (var i = 0; i < lines.length; i++) {
140
- if (!lines[i].trim()) continue;
141
- try {
142
- var rec = JSON.parse(lines[i]);
143
- // Recalculate nextRunAt for scheduled records
144
- if (rec.cron && rec.enabled) {
145
- rec.nextRunAt = nextRunTime(rec.cron);
146
- }
147
- records.push(rec);
148
- } catch (e) {
149
- // skip malformed line
150
- }
151
- }
152
- } catch (e) {
153
- records = [];
154
- }
155
- }
156
-
157
- function save() {
158
- try {
159
- fs.mkdirSync(registryDir, { recursive: true });
160
- var lines = [];
161
- for (var i = 0; i < records.length; i++) {
162
- lines.push(JSON.stringify(records[i]));
163
- }
164
- var tmpPath = registryPath + ".tmp";
165
- fs.writeFileSync(tmpPath, lines.join("\n") + "\n");
166
- fs.renameSync(tmpPath, registryPath);
167
- } catch (e) {
168
- console.error("[loop-registry] Failed to save:", e.message);
169
- }
170
- }
171
-
172
- // --- Timer (scheduled loops only) ---
173
-
174
- function startTimer() {
175
- if (timerId) return;
176
- timerId = setInterval(function () {
177
- tick();
178
- }, CHECK_INTERVAL);
179
- tick();
180
- }
181
-
182
- function stopTimer() {
183
- if (timerId) {
184
- clearInterval(timerId);
185
- timerId = null;
186
- }
187
- }
188
-
189
- function tick() {
190
- var now = Date.now();
191
- var nowMinuteKey = Math.floor(now / 60000);
192
-
193
- for (var i = 0; i < records.length; i++) {
194
- var rec = records[i];
195
- if (!rec.cron) continue; // skip one-off
196
- if (!rec.enabled) continue;
197
- if (!rec.nextRunAt) continue;
198
- if (rec.nextRunAt > now) continue;
199
-
200
- // Avoid double-trigger within same minute
201
- var triggerKey = rec.id + "_" + nowMinuteKey;
202
- if (lastTriggeredMinute[triggerKey]) continue;
203
- lastTriggeredMinute[triggerKey] = true;
204
-
205
- // Clean old trigger keys
206
- var keys = Object.keys(lastTriggeredMinute);
207
- for (var k = 0; k < keys.length; k++) {
208
- var keyParts = keys[k].split("_");
209
- var keyMinute = parseInt(keyParts[keyParts.length - 1], 10);
210
- if (keyMinute < nowMinuteKey - 1) {
211
- delete lastTriggeredMinute[keys[k]];
212
- }
213
- }
214
-
215
- // Update nextRunAt
216
- rec.lastRunAt = now;
217
- rec.nextRunAt = nextRunTime(rec.cron, now);
218
- save();
219
-
220
- console.log("[loop-registry] Triggering scheduled loop: " + rec.name + " (" + rec.id + ")");
221
- if (onTrigger) {
222
- try { onTrigger(rec); } catch (e) {
223
- console.error("[loop-registry] Trigger error:", e.message);
224
- }
225
- }
226
- }
227
- }
228
-
229
- // --- CRUD ---
230
-
231
- function register(data) {
232
- var rec = {
233
- id: data.id || ("loop_" + Date.now() + "_" + require("crypto").randomBytes(3).toString("hex")),
234
- name: data.name || "Untitled",
235
- task: data.task || "",
236
- cron: data.cron || null,
237
- enabled: data.cron ? (data.enabled !== false) : false,
238
- maxIterations: data.maxIterations || 20,
239
- createdAt: Date.now(),
240
- updatedAt: Date.now(),
241
- lastRunAt: null,
242
- lastRunResult: null,
243
- nextRunAt: null,
244
- craftingSessionId: data.craftingSessionId || null,
245
- runs: [],
246
- };
247
- if (rec.cron && rec.enabled) {
248
- rec.nextRunAt = nextRunTime(rec.cron);
249
- }
250
- records.push(rec);
251
- save();
252
- if (onChange) onChange(records);
253
- return rec;
254
- }
255
-
256
- function update(id, data) {
257
- var rec = getById(id);
258
- if (!rec) return null;
259
-
260
- if (data.name !== undefined) rec.name = data.name;
261
- if (data.cron !== undefined) rec.cron = data.cron;
262
- if (data.enabled !== undefined) rec.enabled = data.enabled;
263
- if (data.maxIterations !== undefined) rec.maxIterations = data.maxIterations;
264
- rec.updatedAt = Date.now();
265
- rec.nextRunAt = (rec.cron && rec.enabled) ? nextRunTime(rec.cron) : null;
266
-
267
- save();
268
- if (onChange) onChange(records);
269
- return rec;
270
- }
271
-
272
- function updateRecord(id, data) {
273
- var rec = getById(id);
274
- if (!rec) return null;
275
- var keys = Object.keys(data);
276
- for (var i = 0; i < keys.length; i++) {
277
- rec[keys[i]] = data[keys[i]];
278
- }
279
- save();
280
- return rec;
281
- }
282
-
283
- function remove(id) {
284
- var idx = -1;
285
- for (var i = 0; i < records.length; i++) {
286
- if (records[i].id === id) { idx = i; break; }
287
- }
288
- if (idx === -1) return false;
289
- records.splice(idx, 1);
290
- save();
291
- if (onChange) onChange(records);
292
- return true;
293
- }
294
-
295
- function toggleEnabled(id) {
296
- var rec = getById(id);
297
- if (!rec || !rec.cron) return null; // only toggle scheduled loops
298
- rec.enabled = !rec.enabled;
299
- rec.updatedAt = Date.now();
300
- rec.nextRunAt = rec.enabled ? nextRunTime(rec.cron) : null;
301
- save();
302
- if (onChange) onChange(records);
303
- return rec;
304
- }
305
-
306
- function recordRun(id, result) {
307
- var rec = getById(id);
308
- if (!rec) return;
309
- rec.lastRunAt = result.startedAt || Date.now();
310
- rec.lastRunResult = result.reason || null;
311
- rec.runs.push({
312
- startedAt: result.startedAt || rec.lastRunAt,
313
- finishedAt: Date.now(),
314
- result: result.reason || "unknown",
315
- iterations: result.iterations || 0,
316
- });
317
- // Keep only last 20 run entries
318
- if (rec.runs.length > 20) {
319
- rec.runs = rec.runs.slice(-20);
320
- }
321
- save();
322
- if (onChange) onChange(records);
323
- }
324
-
325
- function getAll() {
326
- return records;
327
- }
328
-
329
- function getById(id) {
330
- for (var i = 0; i < records.length; i++) {
331
- if (records[i].id === id) return records[i];
332
- }
333
- return null;
334
- }
335
-
336
- function getScheduled() {
337
- var result = [];
338
- for (var i = 0; i < records.length; i++) {
339
- if (records[i].cron) result.push(records[i]);
340
- }
341
- return result;
342
- }
343
-
344
- return {
345
- load: load,
346
- save: save,
347
- startTimer: startTimer,
348
- stopTimer: stopTimer,
349
- register: register,
350
- update: update,
351
- updateRecord: updateRecord,
352
- remove: remove,
353
- toggleEnabled: toggleEnabled,
354
- recordRun: recordRun,
355
- getAll: getAll,
356
- getById: getById,
357
- getScheduled: getScheduled,
358
- nextRunTime: nextRunTime,
359
- };
360
- }
361
-
362
- module.exports = { createLoopRegistry: createLoopRegistry };