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.
- package/lib/autonomous-run-record.js +32 -0
- package/lib/autonomous-run-scheduler.js +92 -0
- package/lib/durable-scheduler-record.js +61 -0
- package/lib/durable-scheduler-store.js +185 -0
- package/lib/durable-scheduler.js +495 -0
- package/lib/loop-cron.js +47 -0
- package/lib/loop-durable-schedule.js +159 -0
- package/lib/loop-guidance.js +35 -0
- package/lib/loop-registry-occurrence.js +76 -0
- package/lib/project-autonomous-run.js +498 -0
- package/lib/project-connection.js +4 -1
- package/lib/project-full-access.js +86 -0
- package/lib/project-http.js +3 -1
- package/lib/project-loop-interview.js +163 -0
- package/lib/project-loop.js +150 -64
- package/lib/project-pair-autonomous-stop.js +16 -0
- package/lib/project-pair-owned-stop.js +18 -0
- package/lib/project-pair-task-control.js +14 -1
- package/lib/project-scheduled-messages.js +316 -0
- package/lib/project-scheduled-task-execution.js +236 -0
- package/lib/project-scheduled-tasks.js +483 -0
- package/lib/project-session-pair.js +16 -19
- package/lib/project-sessions.js +60 -28
- package/lib/project-user-message.js +24 -18
- package/lib/project.js +260 -52
- package/lib/public/app.js +60 -23
- package/lib/public/css/home-hub.css +15 -2
- package/lib/public/css/home-sidebar.css +50 -2
- package/lib/public/css/input.css +71 -0
- package/lib/public/css/mates.css +0 -1
- package/lib/public/css/pane.css +58 -7
- package/lib/public/css/rewind.css +11 -2
- package/lib/public/css/scheduled-tasks.css +123 -0
- package/lib/public/css/sticky-notes.css +24 -4
- package/lib/public/index.html +19 -134
- package/lib/public/modules/app-loop-ui.js +4 -8
- package/lib/public/modules/app-messages.js +22 -6
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rate-limit.js +8 -5
- package/lib/public/modules/autonomous-run.js +285 -0
- package/lib/public/modules/home-sidebar.js +16 -8
- package/lib/public/modules/home-surface.js +5 -0
- package/lib/public/modules/input.js +5 -4
- package/lib/public/modules/loop-interview.js +151 -0
- package/lib/public/modules/project-logs.js +2 -2
- package/lib/public/modules/scheduled-message-state.js +46 -0
- package/lib/public/modules/scheduled-task-edit-state.js +7 -0
- package/lib/public/modules/scheduled-task-engine.js +98 -0
- package/lib/public/modules/scheduled-task-form.js +122 -0
- package/lib/public/modules/scheduled-task-recurrence.js +46 -0
- package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
- package/lib/public/modules/scheduled-tasks.js +468 -0
- package/lib/public/modules/scheduler-config.js +6 -52
- package/lib/public/modules/scheduler.js +9 -21
- package/lib/public/modules/sidebar.js +0 -11
- package/lib/public/modules/split-view.js +2 -0
- package/lib/public/modules/sticky-notes-browser.js +1 -1
- package/lib/public/modules/sticky-notes.js +117 -4
- package/lib/public/modules/tool-palette-order.js +2 -3
- package/lib/public/modules/tools.js +5 -2
- package/lib/public/style.css +1 -0
- package/lib/record-revision.js +11 -0
- package/lib/schedule-rule.js +151 -0
- package/lib/schedule-validation.js +124 -0
- package/lib/scheduled-run-state.js +87 -0
- package/lib/scheduled-task-records.js +127 -0
- package/lib/scheduler.js +179 -196
- package/lib/sdk-bridge.js +123 -11
- package/lib/sdk-message-processor.js +3 -0
- package/lib/server-durable-scheduler.js +26 -0
- package/lib/server.js +22 -0
- package/lib/session-pair-factory.js +42 -4
- package/lib/session-pair-query-start.js +22 -0
- package/lib/session-query-tool-bridge.js +52 -0
- package/lib/session-split-groups.js +35 -1
- package/lib/sessions.js +51 -3
- package/lib/users-home-surface-preferences.js +2 -1
- package/lib/users-scheduled-task-preferences.js +44 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +25 -0
- package/lib/yoke/adapters/antigravity.js +1 -0
- package/lib/yoke/adapters/claude-worker.js +7 -4
- package/lib/yoke/adapters/claude.js +31 -1
- package/lib/yoke/adapters/codex.js +3 -0
- package/lib/yoke/claude-user-input.js +2 -0
- package/lib/yoke/index.js +2 -0
- package/lib/yoke/mcp-bridge-server.js +7 -1
- package/lib/yoke/session-tool-transport.js +53 -0
- package/lib/yoke/user-input.js +21 -7
- package/package.json +1 -1
- package/lib/public/modules/app-loop-wizard.js +0 -439
package/lib/scheduler.js
CHANGED
|
@@ -1,137 +1,46 @@
|
|
|
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
1
|
var fs = require("fs");
|
|
11
2
|
var path = require("path");
|
|
3
|
+
var crypto = require("crypto");
|
|
12
4
|
var { CONFIG_DIR } = require("./config");
|
|
13
5
|
var { encodeCwd } = require("./utils");
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
);
|
|
6
|
+
var scheduleRule = require("./schedule-rule");
|
|
7
|
+
var recordRevision = require("./record-revision");
|
|
8
|
+
var createScheduledRunState = require("./scheduled-run-state").createScheduledRunState;
|
|
9
|
+
var attachLoopRegistryOccurrence = require("./loop-registry-occurrence").attachLoopRegistryOccurrence;
|
|
10
|
+
var nextRunTime = require("./loop-cron").nextRunTime;
|
|
11
|
+
|
|
12
|
+
function nextRecordRunTime(record, after) {
|
|
13
|
+
return record.scheduleRule ? scheduleRule.nextScheduleRuleTime(record.scheduleRule, after, record.runs, { startCount: record.scheduleStartCount, dateStartCounts: record.scheduleDateStartCounts }) : (record.cron ? nextRunTime(record.cron, after) : null);
|
|
92
14
|
}
|
|
93
15
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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;
|
|
16
|
+
function oneOffRunTime(record) {
|
|
17
|
+
var date = record.date.split("-");
|
|
18
|
+
var time = record.time.split(":");
|
|
19
|
+
return new Date(Number(date[0]), Number(date[1]) - 1, Number(date[2]), Number(time[0]), Number(time[1]), 0).getTime();
|
|
114
20
|
}
|
|
115
21
|
|
|
116
|
-
// --- Loop Registry factory ---
|
|
117
|
-
|
|
118
22
|
function createLoopRegistry(opts) {
|
|
119
23
|
var cwd = opts.cwd;
|
|
120
|
-
var onTrigger = opts.onTrigger;
|
|
121
|
-
var
|
|
24
|
+
var onTrigger = opts.onTrigger, onChange = opts.onChange;
|
|
25
|
+
var canTrigger = opts.canTrigger, prepareTrigger = opts.prepareTrigger;
|
|
122
26
|
|
|
123
27
|
var encoded = encodeCwd(cwd);
|
|
124
|
-
var
|
|
125
|
-
var
|
|
28
|
+
var registryPath = opts.registryPath || path.join(CONFIG_DIR, "loops", encoded + ".jsonl");
|
|
29
|
+
var registryDir = path.dirname(registryPath);
|
|
126
30
|
|
|
127
31
|
var records = [];
|
|
128
32
|
var timerId = null;
|
|
129
33
|
var CHECK_INTERVAL = 30 * 1000;
|
|
130
34
|
var lastTriggeredMinute = {};
|
|
131
|
-
|
|
132
|
-
|
|
35
|
+
var lastSaveError = null;
|
|
36
|
+
var scheduledRuns = createScheduledRunState({
|
|
37
|
+
getRecord: getById, save: save,
|
|
38
|
+
onChange: function () { if (onChange) onChange(records); },
|
|
39
|
+
});
|
|
40
|
+
var durableOccurrence;
|
|
133
41
|
|
|
134
42
|
function load() {
|
|
43
|
+
var recoveredRun = false;
|
|
135
44
|
try {
|
|
136
45
|
var raw = fs.readFileSync(registryPath, "utf8");
|
|
137
46
|
var lines = raw.trim().split("\n");
|
|
@@ -140,21 +49,24 @@ function createLoopRegistry(opts) {
|
|
|
140
49
|
if (!lines[i].trim()) continue;
|
|
141
50
|
try {
|
|
142
51
|
var rec = JSON.parse(lines[i]);
|
|
143
|
-
|
|
144
|
-
if (rec.cron && rec.
|
|
145
|
-
rec.
|
|
146
|
-
} else if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
rec.nextRunAt =
|
|
52
|
+
if (scheduledRuns.recover(rec)) recoveredRun = true;
|
|
53
|
+
if ((rec.cron || rec.scheduleRule || (rec.source === "schedule" && rec.date && rec.time)) && !rec.scheduleIdentity) {
|
|
54
|
+
rec.scheduleIdentity = crypto.randomUUID(); rec.scheduleEpoch = 1; recoveredRun = true;
|
|
55
|
+
} else if (rec.scheduleIdentity && (!Number.isInteger(rec.scheduleEpoch) || rec.scheduleEpoch < 1)) {
|
|
56
|
+
rec.scheduleEpoch = 1; recoveredRun = true;
|
|
57
|
+
}
|
|
58
|
+
if ((rec.cron || rec.scheduleRule) && rec.enabled) {
|
|
59
|
+
if (!Number.isFinite(rec.nextRunAt)) rec.nextRunAt = nextRecordRunTime(rec);
|
|
60
|
+
if (rec.scheduleRule && !rec.nextRunAt) rec.enabled = false;
|
|
61
|
+
} else if (!rec.cron && !rec.scheduleRule && rec.enabled && rec.date && rec.time && rec.source === "schedule") {
|
|
62
|
+
rec.nextRunAt = oneOffRunTime(rec);
|
|
152
63
|
}
|
|
153
64
|
records.push(rec);
|
|
154
65
|
} catch (e) {
|
|
155
66
|
// skip malformed line
|
|
156
67
|
}
|
|
157
68
|
}
|
|
69
|
+
if (recoveredRun) save();
|
|
158
70
|
} catch (e) {
|
|
159
71
|
records = [];
|
|
160
72
|
}
|
|
@@ -170,12 +82,28 @@ function createLoopRegistry(opts) {
|
|
|
170
82
|
var tmpPath = registryPath + ".tmp";
|
|
171
83
|
fs.writeFileSync(tmpPath, lines.join("\n") + "\n");
|
|
172
84
|
fs.renameSync(tmpPath, registryPath);
|
|
85
|
+
lastSaveError = null;
|
|
86
|
+
return true;
|
|
173
87
|
} catch (e) {
|
|
88
|
+
lastSaveError = e.message || String(e);
|
|
174
89
|
console.error("[loop-registry] Failed to save:", e.message);
|
|
90
|
+
return false;
|
|
175
91
|
}
|
|
176
92
|
}
|
|
177
93
|
|
|
178
|
-
|
|
94
|
+
function notifyOrRollback(rollback) {
|
|
95
|
+
if (!onChange) return true;
|
|
96
|
+
try { onChange(records); return true; }
|
|
97
|
+
catch (error) {
|
|
98
|
+
rollback();
|
|
99
|
+
var syncMessage = error && (error.message || String(error)) || "unknown durable scheduler error";
|
|
100
|
+
var rollbackSaved = save();
|
|
101
|
+
var rollbackMessage = rollbackSaved ? "" : " Registry rollback could not be stored: " + (lastSaveError || "unknown storage error") + ".";
|
|
102
|
+
lastSaveError = "Schedule metadata was rolled back because durable synchronization failed: " + syncMessage + "." + rollbackMessage;
|
|
103
|
+
console.error("[loop-registry] " + lastSaveError);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
179
107
|
|
|
180
108
|
function startTimer() {
|
|
181
109
|
if (timerId) return;
|
|
@@ -201,13 +129,24 @@ function createLoopRegistry(opts) {
|
|
|
201
129
|
if (!rec.enabled) continue;
|
|
202
130
|
if (!rec.nextRunAt) continue;
|
|
203
131
|
if (rec.nextRunAt > now) continue;
|
|
132
|
+
if (canTrigger && canTrigger(rec) === false) continue;
|
|
133
|
+
|
|
134
|
+
if (rec.scheduleRule) {
|
|
135
|
+
var duePrevious = JSON.parse(JSON.stringify(rec));
|
|
136
|
+
var currentDue = nextRecordRunTime(rec, now - 60000);
|
|
137
|
+
if (currentDue !== rec.nextRunAt) {
|
|
138
|
+
rec.nextRunAt = currentDue;
|
|
139
|
+
if (!currentDue) rec.enabled = false;
|
|
140
|
+
if (!save()) recordRevision.restoreRecord(rec, duePrevious);
|
|
141
|
+
if (onChange) onChange(records);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
204
145
|
|
|
205
|
-
// Avoid double-trigger within same minute
|
|
206
146
|
var triggerKey = rec.id + "_" + nowMinuteKey;
|
|
207
147
|
if (lastTriggeredMinute[triggerKey]) continue;
|
|
208
148
|
lastTriggeredMinute[triggerKey] = true;
|
|
209
149
|
|
|
210
|
-
// Clean old trigger keys
|
|
211
150
|
var keys = Object.keys(lastTriggeredMinute);
|
|
212
151
|
for (var k = 0; k < keys.length; k++) {
|
|
213
152
|
var keyParts = keys[k].split("_");
|
|
@@ -217,8 +156,7 @@ function createLoopRegistry(opts) {
|
|
|
217
156
|
}
|
|
218
157
|
}
|
|
219
158
|
|
|
220
|
-
|
|
221
|
-
if (rec.recurrenceEnd) {
|
|
159
|
+
if (!rec.scheduleRule && rec.recurrenceEnd) {
|
|
222
160
|
var shouldDisable = false;
|
|
223
161
|
if (rec.recurrenceEnd.type === "until" && rec.recurrenceEnd.date) {
|
|
224
162
|
var reParts = rec.recurrenceEnd.date.split("-");
|
|
@@ -240,11 +178,9 @@ function createLoopRegistry(opts) {
|
|
|
240
178
|
}
|
|
241
179
|
}
|
|
242
180
|
|
|
243
|
-
|
|
244
|
-
if (rec.intervalEnd) {
|
|
181
|
+
if (!rec.scheduleRule && rec.intervalEnd) {
|
|
245
182
|
var skipTrigger = false;
|
|
246
183
|
if (rec.intervalEnd.type === "until" && rec.intervalEnd.time) {
|
|
247
|
-
// Stop at a specific time of day
|
|
248
184
|
var nowDate = new Date(now);
|
|
249
185
|
var ieParts = rec.intervalEnd.time.split(":");
|
|
250
186
|
var stopH = parseInt(ieParts[0], 10);
|
|
@@ -254,7 +190,6 @@ function createLoopRegistry(opts) {
|
|
|
254
190
|
skipTrigger = true;
|
|
255
191
|
}
|
|
256
192
|
} else if (rec.intervalEnd.type === "after" && rec.intervalEnd.count > 0) {
|
|
257
|
-
// Stop after N runs today
|
|
258
193
|
var todayStart = new Date(now);
|
|
259
194
|
todayStart.setHours(0, 0, 0, 0);
|
|
260
195
|
var todayStartMs = todayStart.getTime();
|
|
@@ -264,7 +199,6 @@ function createLoopRegistry(opts) {
|
|
|
264
199
|
}
|
|
265
200
|
}
|
|
266
201
|
if (skipTrigger) {
|
|
267
|
-
// Advance nextRunAt without triggering
|
|
268
202
|
if (rec.cron) {
|
|
269
203
|
rec.nextRunAt = nextRunTime(rec.cron, now);
|
|
270
204
|
}
|
|
@@ -273,36 +207,42 @@ function createLoopRegistry(opts) {
|
|
|
273
207
|
}
|
|
274
208
|
}
|
|
275
209
|
|
|
276
|
-
|
|
210
|
+
var previous = JSON.parse(JSON.stringify(rec));
|
|
211
|
+
var prepared = prepareTrigger ? prepareTrigger(rec) : null;
|
|
212
|
+
if (prepared && prepared.ok === false) { delete lastTriggeredMinute[triggerKey]; continue; }
|
|
213
|
+
scheduledRuns.prepareRecord(rec, prepared);
|
|
277
214
|
rec.lastRunAt = now;
|
|
278
|
-
if (rec.
|
|
279
|
-
|
|
215
|
+
if (rec.scheduleRule) scheduleRule.recordScheduleStartState(rec, now);
|
|
216
|
+
if (rec.cron || rec.scheduleRule) {
|
|
217
|
+
rec.nextRunAt = nextRecordRunTime(rec, now);
|
|
218
|
+
if (rec.scheduleRule && !rec.nextRunAt) rec.enabled = false;
|
|
280
219
|
} else {
|
|
281
|
-
// One-off schedule: disable after firing
|
|
282
220
|
rec.nextRunAt = null;
|
|
283
221
|
rec.enabled = false;
|
|
284
222
|
}
|
|
285
|
-
save()
|
|
223
|
+
if (!save()) {
|
|
224
|
+
recordRevision.restoreRecord(rec, previous);
|
|
225
|
+
delete lastTriggeredMinute[triggerKey];
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
286
228
|
if (onChange) onChange(records);
|
|
287
229
|
|
|
288
230
|
console.log("[loop-registry] Triggering scheduled loop: " + rec.name + " (" + rec.id + ")");
|
|
289
231
|
if (onTrigger) {
|
|
290
|
-
try { onTrigger(rec); } catch (e) {
|
|
232
|
+
try { onTrigger(rec, prepared); } catch (e) {
|
|
291
233
|
console.error("[loop-registry] Trigger error:", e.message);
|
|
292
234
|
}
|
|
293
235
|
}
|
|
294
236
|
}
|
|
295
237
|
}
|
|
296
238
|
|
|
297
|
-
// --- CRUD ---
|
|
298
|
-
|
|
299
239
|
function register(data) {
|
|
300
240
|
var rec = {
|
|
301
241
|
id: data.id || ("loop_" + Date.now() + "_" + require("crypto").randomBytes(3).toString("hex")),
|
|
302
242
|
name: data.name || "Untitled",
|
|
303
243
|
task: data.task || "",
|
|
304
244
|
cron: data.cron || null,
|
|
305
|
-
enabled: data.cron ? (data.enabled !== false) : false,
|
|
245
|
+
enabled: (data.cron || data.scheduleRule) ? (data.enabled !== false) : false,
|
|
306
246
|
maxIterations: (data.maxIterations >= 1) ? data.maxIterations : 20,
|
|
307
247
|
createdAt: Date.now(),
|
|
308
248
|
updatedAt: Date.now(),
|
|
@@ -320,29 +260,41 @@ function createLoopRegistry(opts) {
|
|
|
320
260
|
recurrenceEnd: data.recurrenceEnd || null,
|
|
321
261
|
mode: data.mode || "loop",
|
|
322
262
|
prompt: data.prompt || null,
|
|
263
|
+
ownerId: data.ownerId || null,
|
|
264
|
+
ownerName: data.ownerName || null,
|
|
265
|
+
sessionId: data.sessionId || null,
|
|
266
|
+
createdViaScheduledTasks: data.createdViaScheduledTasks === true,
|
|
323
267
|
skipIfRunning: data.skipIfRunning !== undefined ? data.skipIfRunning : true,
|
|
324
268
|
intervalEnd: data.intervalEnd || null,
|
|
269
|
+
scheduleRule: data.scheduleRule || null,
|
|
270
|
+
scheduleStartCount: Number(data.scheduleStartCount || 0),
|
|
271
|
+
scheduleDateStartCounts: data.scheduleDateStartCounts && typeof data.scheduleDateStartCounts === "object" ? Object.assign({}, data.scheduleDateStartCounts) : {},
|
|
272
|
+
scheduleIdentity: data.scheduleIdentity || crypto.randomUUID(),
|
|
273
|
+
scheduleEpoch: Number.isInteger(data.scheduleEpoch) && data.scheduleEpoch > 0 ? data.scheduleEpoch : 1,
|
|
274
|
+
execution: data.execution || null,
|
|
275
|
+
activeRun: data.activeRun || null,
|
|
325
276
|
runs: [],
|
|
326
277
|
};
|
|
327
|
-
if (rec.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
var tmParts = rec.time.split(":");
|
|
333
|
-
var runDate = new Date(parseInt(dtParts[0], 10), parseInt(dtParts[1], 10) - 1, parseInt(dtParts[2], 10), parseInt(tmParts[0], 10), parseInt(tmParts[1], 10), 0);
|
|
334
|
-
rec.nextRunAt = runDate.getTime();
|
|
278
|
+
if (rec.createdViaScheduledTasks && data.maxIterations === undefined) delete rec.maxIterations;
|
|
279
|
+
if ((rec.cron || rec.scheduleRule) && rec.enabled) {
|
|
280
|
+
rec.nextRunAt = nextRecordRunTime(rec);
|
|
281
|
+
} else if (!rec.cron && !rec.scheduleRule && rec.date && rec.time && rec.source === "schedule") {
|
|
282
|
+
rec.nextRunAt = oneOffRunTime(rec);
|
|
335
283
|
rec.enabled = true;
|
|
336
284
|
}
|
|
337
285
|
records.push(rec);
|
|
338
|
-
save()
|
|
339
|
-
|
|
286
|
+
if (!save()) {
|
|
287
|
+
records.pop();
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if (!notifyOrRollback(function () { records.pop(); })) return null;
|
|
340
291
|
return rec;
|
|
341
292
|
}
|
|
342
293
|
|
|
343
294
|
function update(id, data) {
|
|
344
295
|
var rec = getById(id);
|
|
345
296
|
if (!rec) return null;
|
|
297
|
+
var previous = Object.assign({}, rec);
|
|
346
298
|
|
|
347
299
|
if (data.name !== undefined) rec.name = data.name;
|
|
348
300
|
if (data.cron !== undefined) rec.cron = data.cron;
|
|
@@ -353,39 +305,54 @@ function createLoopRegistry(opts) {
|
|
|
353
305
|
if (data.recurrenceEnd !== undefined) rec.recurrenceEnd = data.recurrenceEnd;
|
|
354
306
|
if (data.mode !== undefined) rec.mode = data.mode;
|
|
355
307
|
if (data.prompt !== undefined) rec.prompt = data.prompt;
|
|
308
|
+
if (data.task !== undefined) rec.task = data.task;
|
|
309
|
+
if (data.ownerId !== undefined) rec.ownerId = data.ownerId;
|
|
310
|
+
if (data.ownerName !== undefined) rec.ownerName = data.ownerName;
|
|
311
|
+
if (data.sessionId !== undefined) rec.sessionId = data.sessionId;
|
|
356
312
|
if (data.description !== undefined) rec.description = data.description;
|
|
357
313
|
if (data.color !== undefined) rec.color = data.color;
|
|
358
314
|
if (data.allDay !== undefined) rec.allDay = data.allDay;
|
|
359
315
|
if (data.linkedTaskId !== undefined) rec.linkedTaskId = data.linkedTaskId;
|
|
316
|
+
if (data.source !== undefined) rec.source = data.source;
|
|
360
317
|
if (data.skipIfRunning !== undefined) rec.skipIfRunning = data.skipIfRunning;
|
|
361
318
|
if (data.intervalEnd !== undefined) rec.intervalEnd = data.intervalEnd;
|
|
362
|
-
rec.
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
rec.nextRunAt = runD2.getTime();
|
|
370
|
-
rec.enabled = true;
|
|
319
|
+
if (data.scheduleRule !== undefined) rec.scheduleRule = data.scheduleRule;
|
|
320
|
+
if (data.execution !== undefined) rec.execution = data.execution;
|
|
321
|
+
rec.updatedAt = recordRevision.nextRevision(rec);
|
|
322
|
+
if ((rec.cron || rec.scheduleRule) && rec.enabled) {
|
|
323
|
+
rec.nextRunAt = nextRecordRunTime(rec);
|
|
324
|
+
} else if (!rec.cron && !rec.scheduleRule && rec.enabled && rec.date && rec.time && rec.source === "schedule") {
|
|
325
|
+
rec.nextRunAt = oneOffRunTime(rec);
|
|
371
326
|
} else {
|
|
372
327
|
rec.nextRunAt = null;
|
|
373
328
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (
|
|
329
|
+
var sameTiming = previous.cron === rec.cron && previous.date === rec.date && previous.time === rec.time && JSON.stringify(previous.scheduleRule || null) === JSON.stringify(rec.scheduleRule || null);
|
|
330
|
+
if (!sameTiming || previous.enabled !== rec.enabled) rec.scheduleEpoch = Math.max(1, Number(previous.scheduleEpoch) || 1) + 1;
|
|
331
|
+
if (previous.enabled && rec.enabled && previous.nextRunAt && sameTiming) rec.nextRunAt = previous.nextRunAt;
|
|
332
|
+
if (data.ownerId) { delete rec.needsOwner; delete rec.setupRequired; }
|
|
333
|
+
|
|
334
|
+
if (!save()) {
|
|
335
|
+
recordRevision.restoreRecord(rec, previous);
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
if (!notifyOrRollback(function () { recordRevision.restoreRecord(rec, previous); })) return null;
|
|
377
339
|
return rec;
|
|
378
340
|
}
|
|
379
341
|
|
|
380
342
|
function updateRecord(id, data) {
|
|
381
343
|
var rec = getById(id);
|
|
382
344
|
if (!rec) return null;
|
|
345
|
+
var previous = Object.assign({}, rec);
|
|
383
346
|
var keys = Object.keys(data);
|
|
384
347
|
for (var i = 0; i < keys.length; i++) {
|
|
385
348
|
rec[keys[i]] = data[keys[i]];
|
|
386
349
|
}
|
|
387
|
-
|
|
388
|
-
if (
|
|
350
|
+
rec.updatedAt = recordRevision.nextRevision(previous);
|
|
351
|
+
if (!save()) {
|
|
352
|
+
recordRevision.restoreRecord(rec, previous);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
if (!notifyOrRollback(function () { recordRevision.restoreRecord(rec, previous); })) return null;
|
|
389
356
|
return rec;
|
|
390
357
|
}
|
|
391
358
|
|
|
@@ -395,40 +362,33 @@ function createLoopRegistry(opts) {
|
|
|
395
362
|
if (records[i].id === id) { idx = i; break; }
|
|
396
363
|
}
|
|
397
364
|
if (idx === -1) return false;
|
|
398
|
-
records.splice(idx, 1);
|
|
399
|
-
save();
|
|
400
|
-
if (
|
|
365
|
+
var removed = records.splice(idx, 1)[0];
|
|
366
|
+
if (!save()) { records.splice(idx, 0, removed); return false; }
|
|
367
|
+
if (!notifyOrRollback(function () { records.splice(idx, 0, removed); })) return false;
|
|
401
368
|
return true;
|
|
402
369
|
}
|
|
403
370
|
|
|
404
371
|
function toggleEnabled(id) {
|
|
405
372
|
var rec = getById(id);
|
|
406
|
-
|
|
373
|
+
var oneOffSchedule = rec && !rec.cron && !rec.scheduleRule && rec.date && rec.time && rec.source === "schedule";
|
|
374
|
+
if (!rec || (!rec.cron && !rec.scheduleRule && !oneOffSchedule)) return null;
|
|
375
|
+
var previous = Object.assign({}, rec);
|
|
407
376
|
rec.enabled = !rec.enabled;
|
|
408
|
-
rec.
|
|
409
|
-
rec.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (!
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
rec.runs.push({
|
|
421
|
-
startedAt: result.startedAt || rec.lastRunAt,
|
|
422
|
-
finishedAt: Date.now(),
|
|
423
|
-
result: result.reason || "unknown",
|
|
424
|
-
iterations: result.iterations || 0,
|
|
425
|
-
});
|
|
426
|
-
// Keep only last 20 run entries
|
|
427
|
-
if (rec.runs.length > 20) {
|
|
428
|
-
rec.runs = rec.runs.slice(-20);
|
|
377
|
+
rec.scheduleEpoch = Math.max(1, Number(rec.scheduleEpoch) || 1) + 1;
|
|
378
|
+
rec.updatedAt = recordRevision.nextRevision(rec);
|
|
379
|
+
if (!rec.enabled) {
|
|
380
|
+
rec.nextRunAt = null;
|
|
381
|
+
} else if (rec.cron || rec.scheduleRule) {
|
|
382
|
+
rec.nextRunAt = nextRecordRunTime(rec);
|
|
383
|
+
} else {
|
|
384
|
+
rec.nextRunAt = oneOffRunTime(rec);
|
|
385
|
+
}
|
|
386
|
+
if (!save()) {
|
|
387
|
+
recordRevision.restoreRecord(rec, previous);
|
|
388
|
+
return null;
|
|
429
389
|
}
|
|
430
|
-
|
|
431
|
-
|
|
390
|
+
if (!notifyOrRollback(function () { recordRevision.restoreRecord(rec, previous); })) return null;
|
|
391
|
+
return rec;
|
|
432
392
|
}
|
|
433
393
|
|
|
434
394
|
function getAll() {
|
|
@@ -445,25 +405,48 @@ function createLoopRegistry(opts) {
|
|
|
445
405
|
function getScheduled() {
|
|
446
406
|
var result = [];
|
|
447
407
|
for (var i = 0; i < records.length; i++) {
|
|
448
|
-
if (records[i].cron) result.push(records[i]);
|
|
408
|
+
if (records[i].cron || records[i].scheduleRule) result.push(records[i]);
|
|
449
409
|
}
|
|
450
410
|
return result;
|
|
451
411
|
}
|
|
452
412
|
|
|
413
|
+
durableOccurrence = attachLoopRegistryOccurrence({
|
|
414
|
+
getById: getById,
|
|
415
|
+
save: save,
|
|
416
|
+
changed: function () { if (onChange) onChange(records); },
|
|
417
|
+
lastSaveError: function () { return lastSaveError; },
|
|
418
|
+
prepareTrigger: prepareTrigger,
|
|
419
|
+
onTrigger: onTrigger,
|
|
420
|
+
scheduledRuns: scheduledRuns,
|
|
421
|
+
scheduleRule: scheduleRule,
|
|
422
|
+
nextRecordRunTime: nextRecordRunTime,
|
|
423
|
+
});
|
|
424
|
+
|
|
453
425
|
return {
|
|
454
426
|
load: load,
|
|
455
427
|
save: save,
|
|
456
428
|
startTimer: startTimer,
|
|
457
429
|
stopTimer: stopTimer,
|
|
430
|
+
tick: tick,
|
|
458
431
|
register: register,
|
|
459
432
|
update: update,
|
|
460
433
|
updateRecord: updateRecord,
|
|
461
434
|
remove: remove,
|
|
462
435
|
toggleEnabled: toggleEnabled,
|
|
463
|
-
|
|
436
|
+
normalizeDurableOccurrence: durableOccurrence.normalize,
|
|
437
|
+
markNeedsOwner: durableOccurrence.markNeedsOwner,
|
|
438
|
+
prepareDurableOccurrence: durableOccurrence.prepare,
|
|
439
|
+
triggerDurableOccurrence: durableOccurrence.trigger,
|
|
440
|
+
completeDurableOccurrence: durableOccurrence.complete,
|
|
441
|
+
reconcileDurableOccurrence: durableOccurrence.reconcile,
|
|
442
|
+
recordRun: scheduledRuns.recordLegacy,
|
|
443
|
+
beginRun: scheduledRuns.begin,
|
|
444
|
+
updateRun: scheduledRuns.update,
|
|
445
|
+
completeRun: scheduledRuns.complete,
|
|
464
446
|
getAll: getAll,
|
|
465
447
|
getById: getById,
|
|
466
448
|
getScheduled: getScheduled,
|
|
449
|
+
getLastSaveError: function () { return lastSaveError; },
|
|
467
450
|
nextRunTime: nextRunTime,
|
|
468
451
|
};
|
|
469
452
|
}
|