@framers/agentos-ext-triggers 0.0.0-stage → 0.1.0
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/LICENSE +98 -0
- package/README.md +198 -2
- package/dist/contract.d.ts +262 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -0
- package/dist/cron.d.ts +36 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +125 -0
- package/dist/cron.js.map +1 -0
- package/dist/housekeeping.d.ts +20 -0
- package/dist/housekeeping.d.ts.map +1 -0
- package/dist/housekeeping.js +33 -0
- package/dist/housekeeping.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/names.d.ts +9 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +9 -0
- package/dist/names.js.map +1 -0
- package/dist/report.d.ts +27 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +94 -0
- package/dist/report.js.map +1 -0
- package/dist/rows.d.ts +87 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +52 -0
- package/dist/rows.js.map +1 -0
- package/dist/scheduler.d.ts +44 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +743 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/store/ids.d.ts +3 -0
- package/dist/store/ids.d.ts.map +1 -0
- package/dist/store/ids.js +6 -0
- package/dist/store/ids.js.map +1 -0
- package/dist/store/migrations.d.ts +30 -0
- package/dist/store/migrations.d.ts.map +1 -0
- package/dist/store/migrations.js +26 -0
- package/dist/store/migrations.js.map +1 -0
- package/dist/store/prefix.d.ts +23 -0
- package/dist/store/prefix.d.ts.map +1 -0
- package/dist/store/prefix.js +37 -0
- package/dist/store/prefix.js.map +1 -0
- package/dist/store/sql/index.d.ts +21 -0
- package/dist/store/sql/index.d.ts.map +1 -0
- package/dist/store/sql/index.js +157 -0
- package/dist/store/sql/index.js.map +1 -0
- package/dist/watcher.d.ts +28 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +279 -0
- package/dist/watcher.js.map +1 -0
- package/dist/words.d.ts +12 -0
- package/dist/words.d.ts.map +1 -0
- package/dist/words.js +57 -0
- package/dist/words.js.map +1 -0
- package/manifest.json +39 -0
- package/package.json +68 -3
package/dist/watcher.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { WATCH_ROW } from './names.js';
|
|
2
|
+
import { num, viewOf } from './rows.js';
|
|
3
|
+
import { randomHex } from './store/ids.js';
|
|
4
|
+
import { dialectOf } from './store/migrations.js';
|
|
5
|
+
import { DEFAULT_PREFIX, requireDialectPrefix, requirePrefix, tableName } from './store/prefix.js';
|
|
6
|
+
/** How often the watcher passes when the host gives no interval. */
|
|
7
|
+
export const DEFAULT_WATCH_EVERY_MS = 60_000;
|
|
8
|
+
/** How long a host's handler may run before the watcher gives it up as a throw, when the host gives no bound. */
|
|
9
|
+
export const DEFAULT_HANDLER_TIMEOUT_MS = 30_000;
|
|
10
|
+
const DAY_MS = 86_400_000;
|
|
11
|
+
/**
|
|
12
|
+
* The stall watcher (spec 4.2 steps 5 and 6): under its own lease row `__watch` it finds stalled jobs and calls `onStalled`
|
|
13
|
+
* once a stall, storing the handler's reference on the row; `onRecovered` once when the job runs again; `onFailedAgain`
|
|
14
|
+
* once a day for a job that stays failed. Paused and failed jobs are never judged stalled, and neither is a scheduled job
|
|
15
|
+
* with nothing owed (a one-shot that ran, or a job whose next due time or retry is still ahead); a scheduled job's quiet is
|
|
16
|
+
* measured from its last run or the time it became owed, whichever is later. A pass renews the watch lease while it runs and
|
|
17
|
+
* stops when another holder took it. A handler call that has not settled within `handlerTimeoutMs` is given up as a throw, so
|
|
18
|
+
* a handler that never settles never holds the pass and its lease. `internals.arm: false` arms no interval: the caller drives `pass()`.
|
|
19
|
+
*/
|
|
20
|
+
export function createWatcher(options, internals = {}) {
|
|
21
|
+
const db = options.db;
|
|
22
|
+
const prefix = requireDialectPrefix(requirePrefix(options.prefix ?? DEFAULT_PREFIX), dialectOf(db));
|
|
23
|
+
const t = (name) => tableName(prefix, name);
|
|
24
|
+
const now = options.now ?? Date.now;
|
|
25
|
+
const everyMs = options.everyMs ?? DEFAULT_WATCH_EVERY_MS;
|
|
26
|
+
if (!(Number.isInteger(everyMs) && everyMs > 0))
|
|
27
|
+
throw new RangeError('the watcher\'s interval is a whole number of milliseconds above 0');
|
|
28
|
+
const handlerTimeoutMs = options.handlerTimeoutMs ?? DEFAULT_HANDLER_TIMEOUT_MS;
|
|
29
|
+
if (!(Number.isInteger(handlerTimeoutMs) && handlerTimeoutMs > 0))
|
|
30
|
+
throw new RangeError('the watcher\'s handler bound is a whole number of milliseconds above 0');
|
|
31
|
+
const logger = options.logger;
|
|
32
|
+
const holder = `${options.process}:${randomHex(4)}`;
|
|
33
|
+
const stalled = new Set();
|
|
34
|
+
const recovered = new Set();
|
|
35
|
+
const failedAgain = new Set();
|
|
36
|
+
let timer = null;
|
|
37
|
+
/** The pass in progress, which a second call and `stop()` wait on rather than start another. */
|
|
38
|
+
let current = null;
|
|
39
|
+
/**
|
|
40
|
+
* A host's handler call, rejected once it has run `handlerTimeoutMs` without settling: the caller counts it as a throw, so the
|
|
41
|
+
* stall, recovery or repeat stays unacknowledged and the next pass calls it again (the handlers are idempotent). A call given up
|
|
42
|
+
* is one `watch_handler_timed_out` line, and what it answers later is not stored.
|
|
43
|
+
*/
|
|
44
|
+
function bounded(work, job, handler) {
|
|
45
|
+
let timer;
|
|
46
|
+
const limit = new Promise((_, reject) => {
|
|
47
|
+
timer = setTimeout(() => {
|
|
48
|
+
logger.warn('watch_handler_timed_out', { job, handler });
|
|
49
|
+
reject(new Error('handler timed out'));
|
|
50
|
+
}, handlerTimeoutMs);
|
|
51
|
+
});
|
|
52
|
+
return Promise.race([work, limit]).finally(() => clearTimeout(timer));
|
|
53
|
+
}
|
|
54
|
+
const view = async (row, at) => {
|
|
55
|
+
const missed = await db.get(`SELECT COUNT(*) AS n FROM ${t('missed')} WHERE job = ? AND due_at >= ?`, [row.name, at - DAY_MS]);
|
|
56
|
+
return viewOf(row, at, Number(missed?.n ?? 0), null);
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Tells the host of a stall and acknowledges it on the row only once every handler settled: `stall_ref` holds the reference
|
|
60
|
+
* the handlers answered, or '' for "told, no reference"; NULL means "not yet told", and the next pass tells again. The host's
|
|
61
|
+
* handler is idempotent by `(job.name, since)`, so a second call finds the record the first one made.
|
|
62
|
+
*/
|
|
63
|
+
async function tell(row, since, at) {
|
|
64
|
+
const job = await view({ ...row, stall_since: since }, at);
|
|
65
|
+
let ref = null;
|
|
66
|
+
let ok = true;
|
|
67
|
+
for (const handler of stalled) {
|
|
68
|
+
try {
|
|
69
|
+
ref = (await bounded(handler(job, since), row.name, 'stalled')) ?? ref;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
ok = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (ok)
|
|
76
|
+
await db.run(`UPDATE ${t('jobs')} SET stall_ref = ?, updated_at = ? WHERE name = ? AND stall_since = ?`, [ref ?? '', at, row.name, since]);
|
|
77
|
+
}
|
|
78
|
+
async function ensureRow(at) {
|
|
79
|
+
await db.run(`INSERT INTO ${t('jobs')} (name, title, process, kind, schedule, schedule_words, rule, stall_after_ms, safe_to_repeat, retries, timeout_ms, legal_duty, enabled, registered_at, updated_at)
|
|
80
|
+
VALUES (?, 'The stall watcher', ?, 'scheduled', NULL, '', 'skip', 0, 1, 0, 0, 0, 0, ?, ?) ON CONFLICT (name) DO NOTHING`, [WATCH_ROW, options.process, at, at]);
|
|
81
|
+
}
|
|
82
|
+
/** One statement claims the watch row for twice the pass interval; one row changed means this pass runs. */
|
|
83
|
+
async function claim(at) {
|
|
84
|
+
const token = randomHex(16);
|
|
85
|
+
const changed = await db.run(`UPDATE ${t('jobs')} SET lease_holder = ?, lease_token = ?, lease_until = ?, updated_at = ? WHERE name = ? AND (lease_until IS NULL OR lease_until < ?)`, [holder, token, at + 2 * everyMs, at, WATCH_ROW, at]);
|
|
86
|
+
return Number(changed.changes) === 1 ? token : null;
|
|
87
|
+
}
|
|
88
|
+
async function release(token) {
|
|
89
|
+
await db.run(`UPDATE ${t('jobs')} SET lease_holder = NULL, lease_token = NULL, lease_until = NULL WHERE name = ? AND lease_token = ?`, [WATCH_ROW, token]);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The time a job's quiet is measured from, or null when it cannot be stalled: a paused or failed job, a gap of 0, and a
|
|
93
|
+
* scheduled job with nothing owed (finished, or its next due time or its retry's readiness still ahead). A reports-only job
|
|
94
|
+
* is measured from its last report. A scheduled job is measured from its last run (its registration when it never ran) or
|
|
95
|
+
* the time it became owed, whichever is later: a job due less often than its gap, a one-shot registered days ahead and a
|
|
96
|
+
* retry waiting out a backoff are stalled only once they have stayed owed for longer than their gap with no run, never
|
|
97
|
+
* while a due time that came less than a gap ago waits for its run.
|
|
98
|
+
*/
|
|
99
|
+
function quietFrom(row, at) {
|
|
100
|
+
if (row.paused_by !== null || row.failed_at !== null || Number(row.stall_after_ms) <= 0)
|
|
101
|
+
return null;
|
|
102
|
+
const lastAt = num(row.last_run_at) ?? Number(row.registered_at);
|
|
103
|
+
if (row.kind === 'reports_only')
|
|
104
|
+
return lastAt;
|
|
105
|
+
const nextDue = num(row.next_due_at);
|
|
106
|
+
if (row.enabled !== 1 || nextDue === null)
|
|
107
|
+
return null;
|
|
108
|
+
// A retry serves `next_due_at` once `retry_at` comes: until then the job is owed nothing.
|
|
109
|
+
const retryAt = num(row.retry_at);
|
|
110
|
+
const owedAt = retryAt === null ? nextDue : Math.max(nextDue, retryAt);
|
|
111
|
+
return owedAt > at ? null : Math.max(lastAt, owedAt);
|
|
112
|
+
}
|
|
113
|
+
async function judge(row, at) {
|
|
114
|
+
const since = num(row.stall_since);
|
|
115
|
+
const lastAt = num(row.last_run_at) ?? Number(row.registered_at);
|
|
116
|
+
if (since === null) {
|
|
117
|
+
const from = quietFrom(row, at);
|
|
118
|
+
const gap = Number(row.stall_after_ms);
|
|
119
|
+
if (from === null || at - from <= gap)
|
|
120
|
+
return;
|
|
121
|
+
// The stall is marked first under the watch lease, so a second watcher or a restart never opens a second record for it;
|
|
122
|
+
// the host is told, and the row acknowledges it only once the handlers settled (`tell`). The mark holds only while the row
|
|
123
|
+
// still reads stalled: a run that settled, a pause or a failure since the read, or a registration that moved its next due
|
|
124
|
+
// time ahead or changed its gap, leaves it unmarked. Every time the mark checks is older than `at - gap`, as `quietFrom` judged.
|
|
125
|
+
const quietBefore = at - gap;
|
|
126
|
+
const lastRun = row.last_run_at === null ? 'last_run_at IS NULL' : 'last_run_at = ?';
|
|
127
|
+
const marked = await db.run(`UPDATE ${t('jobs')} SET stall_since = ?, updated_at = ? WHERE name = ? AND stall_since IS NULL AND paused_by IS NULL AND failed_at IS NULL AND stall_after_ms = ? AND ${lastRun}
|
|
128
|
+
AND (kind = 'reports_only' OR (enabled = 1 AND next_due_at IS NOT NULL AND next_due_at < ? AND (retry_at IS NULL OR retry_at < ?)))`, [lastAt, at, row.name, gap, ...(row.last_run_at === null ? [] : [lastAt]), quietBefore, quietBefore]);
|
|
129
|
+
if (Number(marked.changes) !== 1)
|
|
130
|
+
return;
|
|
131
|
+
logger.warn('job_stalled', { job: row.name, gap_minutes: Math.round((at - from) / 60_000) });
|
|
132
|
+
await tell(row, lastAt, at);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (lastAt <= since) {
|
|
136
|
+
// Marked and not yet acknowledged (a throw, or a crash between the mark and the reference): told again.
|
|
137
|
+
if (row.stall_ref === null)
|
|
138
|
+
await tell(row, since, at);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Recovered. A stall never acknowledged (the host's handler opened its record, then threw before the reference was
|
|
142
|
+
// stored) is told again first, so the idempotent handler answers the record it opened and the row stores it; while it
|
|
143
|
+
// stays unacknowledged the stall is kept and nothing is cleared.
|
|
144
|
+
let ref = row.stall_ref;
|
|
145
|
+
if (ref === null) {
|
|
146
|
+
await tell(row, since, at);
|
|
147
|
+
// The reference of this stall: a row that holds another stall now (another watcher closed this one and opened the next
|
|
148
|
+
// while this pass waited) answers nothing, and that stall is its own watcher's to recover.
|
|
149
|
+
ref = (await db.get(`SELECT stall_ref FROM ${t('jobs')} WHERE name = ? AND stall_since = ?`, [row.name, since]))?.stall_ref ?? null;
|
|
150
|
+
if (ref === null)
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Then the recovery handlers, and the stall is cleared only once every one settled, so a handler that threw is called
|
|
154
|
+
// again on the next pass with the same reference (idempotent by it).
|
|
155
|
+
const job = await view({ ...row, stall_since: null, stall_ref: null }, at);
|
|
156
|
+
let ok = true;
|
|
157
|
+
for (const handler of recovered) {
|
|
158
|
+
try {
|
|
159
|
+
await bounded(handler(job, since, ref === '' ? null : ref), row.name, 'recovered');
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
ok = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (ok)
|
|
166
|
+
await db.run(`UPDATE ${t('jobs')} SET stall_since = NULL, stall_ref = NULL, updated_at = ? WHERE name = ? AND stall_since = ?`, [at, row.name, since]);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The daily repeat for a job that stays failed: the handlers first (idempotent by the day count), `alerted_at` moved only once
|
|
170
|
+
* every one settled. The row is read again at the job's turn, so a job resumed (or failed again) since the pass read it is
|
|
171
|
+
* judged as it is now, never told from the pass's first read.
|
|
172
|
+
*/
|
|
173
|
+
async function remind(read, at) {
|
|
174
|
+
if (read.failed_at === null)
|
|
175
|
+
return;
|
|
176
|
+
const row = await db.get(`SELECT * FROM ${t('jobs')} WHERE name = ?`, [read.name]);
|
|
177
|
+
if (!row)
|
|
178
|
+
return;
|
|
179
|
+
const alertedAt = num(row.alerted_at);
|
|
180
|
+
const failedAt = num(row.failed_at);
|
|
181
|
+
if (failedAt === null || alertedAt === null || at - alertedAt < DAY_MS)
|
|
182
|
+
return;
|
|
183
|
+
const days = Math.floor((at - failedAt) / DAY_MS);
|
|
184
|
+
const job = await view(row, at);
|
|
185
|
+
let ok = true;
|
|
186
|
+
for (const handler of failedAgain) {
|
|
187
|
+
try {
|
|
188
|
+
await bounded(handler(job, days), row.name, 'failed_again');
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
ok = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (ok)
|
|
195
|
+
await db.run(`UPDATE ${t('jobs')} SET alerted_at = ?, updated_at = ? WHERE name = ? AND alerted_at = ?`, [at, at, row.name, alertedAt]);
|
|
196
|
+
}
|
|
197
|
+
async function passOnce() {
|
|
198
|
+
const at = now();
|
|
199
|
+
await ensureRow(at);
|
|
200
|
+
const token = await claim(at);
|
|
201
|
+
if (token === null)
|
|
202
|
+
return;
|
|
203
|
+
let held = true;
|
|
204
|
+
/** Renews the watch lease under this pass's token; a renewal that changes no row, or that the database refuses, ends the hold. */
|
|
205
|
+
const renew = async () => {
|
|
206
|
+
if (!held)
|
|
207
|
+
return false;
|
|
208
|
+
try {
|
|
209
|
+
const renewedAt = now();
|
|
210
|
+
const changed = await db.run(`UPDATE ${t('jobs')} SET lease_until = ?, updated_at = ? WHERE name = ? AND lease_token = ?`, [renewedAt + 2 * everyMs, renewedAt, WATCH_ROW, token]);
|
|
211
|
+
held = Number(changed.changes) === 1;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
held = false;
|
|
215
|
+
}
|
|
216
|
+
return held;
|
|
217
|
+
};
|
|
218
|
+
// The lease is renewed every half interval while the pass runs, a host's handler awaited included, so a slow handler never
|
|
219
|
+
// lets a second watcher take the row and tell the same stall at once; each handler call is bounded (`bounded`), so the pass
|
|
220
|
+
// ends and the renewals stop even when a handler never settles.
|
|
221
|
+
const renewer = setInterval(() => void renew(), Math.max(1, Math.floor(everyMs / 2)));
|
|
222
|
+
renewer.unref?.();
|
|
223
|
+
try {
|
|
224
|
+
const rows = await db.all(`SELECT * FROM ${t('jobs')} WHERE name <> ? ORDER BY name`, [WATCH_ROW]);
|
|
225
|
+
for (const row of rows) {
|
|
226
|
+
// Renewed before each job too: a lease another holder took (this process lost the database for two intervals) means
|
|
227
|
+
// that holder judges from here, so this pass judges no further job.
|
|
228
|
+
if (!(await renew())) {
|
|
229
|
+
logger.warn('watch_lease_lost', {});
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
await judge(row, at);
|
|
233
|
+
await remind(row, at);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
clearInterval(renewer);
|
|
238
|
+
await release(token);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function pass() {
|
|
242
|
+
current ??= passOnce().finally(() => { current = null; });
|
|
243
|
+
return current;
|
|
244
|
+
}
|
|
245
|
+
/** A pass the interval began: a failure is one log line, never an unhandled rejection in the host's process. */
|
|
246
|
+
function timedPass() {
|
|
247
|
+
pass().catch((error) => {
|
|
248
|
+
logger.error('watch_failed', { kind: options.describe?.(error)?.kind ?? 'error' });
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const subscribe = (set) => (handler) => {
|
|
252
|
+
set.add(handler);
|
|
253
|
+
return () => {
|
|
254
|
+
set.delete(handler);
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
start() {
|
|
259
|
+
if (timer !== null || internals.arm === false)
|
|
260
|
+
return;
|
|
261
|
+
timer = setInterval(timedPass, everyMs);
|
|
262
|
+
timer.unref?.();
|
|
263
|
+
timedPass();
|
|
264
|
+
},
|
|
265
|
+
async stop() {
|
|
266
|
+
if (timer !== null) {
|
|
267
|
+
clearInterval(timer);
|
|
268
|
+
timer = null;
|
|
269
|
+
}
|
|
270
|
+
// The pass in progress finishes and frees the watch lease; its failure was logged where it began.
|
|
271
|
+
await current?.catch(() => undefined);
|
|
272
|
+
},
|
|
273
|
+
onStalled: subscribe(stalled),
|
|
274
|
+
onRecovered: subscribe(recovered),
|
|
275
|
+
onFailedAgain: subscribe(failedAgain),
|
|
276
|
+
pass,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"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"}
|
package/dist/words.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `scheduleWords`: the sentence a schedule reads as on a jobs page, in UTC. A product
|
|
3
|
+
* may show its own words instead; these are the package's.
|
|
4
|
+
*/
|
|
5
|
+
import type { Schedule } from './contract.js';
|
|
6
|
+
/**
|
|
7
|
+
* The six forms the Jobs page shows: "Every 5 minutes", "Every hour at :40", "Daily at 00:20 UTC", "Weekly on Monday at 08:23 UTC", "Every 8 days", "Once, 4 Nov 2026 09:00 UTC"; any other cron reads "Cron <expr> UTC".
|
|
8
|
+
* Throws a `RangeError` for an interval that is not a whole number of milliseconds above 0 or a time that is not finite, and `CronRefused` for a cron expression the parser refuses, so it never reads them as words.
|
|
9
|
+
* `register` checks more (`requireSchedule`: an interval of at least `tickMs`, a finite anchor), so a schedule these words describe can still be refused there.
|
|
10
|
+
*/
|
|
11
|
+
export declare function scheduleWords(schedule: Schedule): string;
|
|
12
|
+
//# sourceMappingURL=words.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"words.d.ts","sourceRoot":"","sources":["../src/words.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAe9C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CA8BxD"}
|
package/dist/words.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { parseCronExpression } from './cron.js';
|
|
2
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
3
|
+
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
4
|
+
const two = (n) => String(n).padStart(2, '0');
|
|
5
|
+
function everyWords(ms) {
|
|
6
|
+
if (ms % 86_400_000 === 0)
|
|
7
|
+
return ms === 86_400_000 ? 'Every day' : `Every ${ms / 86_400_000} days`;
|
|
8
|
+
if (ms % 3_600_000 === 0)
|
|
9
|
+
return ms === 3_600_000 ? 'Every hour' : `Every ${ms / 3_600_000} hours`;
|
|
10
|
+
if (ms % 60_000 === 0)
|
|
11
|
+
return ms === 60_000 ? 'Every minute' : `Every ${ms / 60_000} minutes`;
|
|
12
|
+
// The interval as written: 10,500 ms reads 10.5 seconds, never a rounded 11.
|
|
13
|
+
return ms === 1_000 ? 'Every second' : `Every ${ms / 1_000} seconds`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The six forms the Jobs page shows: "Every 5 minutes", "Every hour at :40", "Daily at 00:20 UTC", "Weekly on Monday at 08:23 UTC", "Every 8 days", "Once, 4 Nov 2026 09:00 UTC"; any other cron reads "Cron <expr> UTC".
|
|
17
|
+
* Throws a `RangeError` for an interval that is not a whole number of milliseconds above 0 or a time that is not finite, and `CronRefused` for a cron expression the parser refuses, so it never reads them as words.
|
|
18
|
+
* `register` checks more (`requireSchedule`: an interval of at least `tickMs`, a finite anchor), so a schedule these words describe can still be refused there.
|
|
19
|
+
*/
|
|
20
|
+
export function scheduleWords(schedule) {
|
|
21
|
+
if (schedule.kind === 'every') {
|
|
22
|
+
if (!Number.isInteger(schedule.everyMs) || schedule.everyMs <= 0)
|
|
23
|
+
throw new RangeError('an every schedule needs a whole number of milliseconds above 0');
|
|
24
|
+
return everyWords(schedule.everyMs);
|
|
25
|
+
}
|
|
26
|
+
if (schedule.kind === 'at') {
|
|
27
|
+
if (!Number.isFinite(schedule.at))
|
|
28
|
+
throw new RangeError('an at schedule needs a finite time');
|
|
29
|
+
const d = new Date(schedule.at);
|
|
30
|
+
return `Once, ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()} ${two(d.getUTCHours())}:${two(d.getUTCMinutes())} UTC`;
|
|
31
|
+
}
|
|
32
|
+
const parts = schedule.expr.trim().split(/\s+/);
|
|
33
|
+
const fields = parseCronExpression(schedule.expr);
|
|
34
|
+
const [minute, hour, dom, month, dow] = parts;
|
|
35
|
+
const one = (field) => (field.size === 1 ? [...field][0] : undefined);
|
|
36
|
+
const m = one(fields.minutes);
|
|
37
|
+
const h = one(fields.hours);
|
|
38
|
+
const all = hour === '*' && dom === '*' && month === '*' && dow === '*';
|
|
39
|
+
if (minute === '*' && all)
|
|
40
|
+
return 'Every minute';
|
|
41
|
+
// A step reads as an interval only when it divides the hour: */7 runs at :56 and again at :00, and */60 once an hour.
|
|
42
|
+
const step = /^\*\/\d+$/.test(minute) ? Number(minute.slice(2)) : 0;
|
|
43
|
+
if (all && step > 0 && step < 60 && 60 % step === 0)
|
|
44
|
+
return step === 1 ? 'Every minute' : `Every ${step} minutes`;
|
|
45
|
+
if (m !== undefined && hour === '*' && dom === '*' && month === '*' && dow === '*')
|
|
46
|
+
return `Every hour at :${two(m)}`;
|
|
47
|
+
if (m !== undefined && h !== undefined && dom === '*' && month === '*' && dow === '*')
|
|
48
|
+
return `Daily at ${two(h)}:${two(m)} UTC`;
|
|
49
|
+
if (m !== undefined && h !== undefined && dom === '*' && month === '*' && one(fields.daysOfWeek) !== undefined) {
|
|
50
|
+
return `Weekly on ${DAYS[one(fields.daysOfWeek)]} at ${two(h)}:${two(m)} UTC`;
|
|
51
|
+
}
|
|
52
|
+
if (m !== undefined && h !== undefined && one(fields.daysOfMonth) !== undefined && month === '*' && dow === '*') {
|
|
53
|
+
return `Monthly on day ${one(fields.daysOfMonth)} at ${two(h)}:${two(m)} UTC`;
|
|
54
|
+
}
|
|
55
|
+
return `Cron ${parts.join(' ')} UTC`;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=words.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"words.js","sourceRoot":"","sources":["../src/words.ts"],"names":[],"mappings":"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"}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
|
|
3
|
+
"id": "com.framers.system.triggers",
|
|
4
|
+
"name": "Triggers",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"description": "A scheduler of record over the SQL storage adapter: job rows, a lease with a claim token, runs, missed runs and their rule, pause, resume, a hand run, bounded retries and a failed state, a stall watcher, reports-only jobs and an audit hook, with migrations on PostgreSQL and SQLite.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Frame",
|
|
9
|
+
"email": "team@frame.dev",
|
|
10
|
+
"url": "https://frame.dev"
|
|
11
|
+
},
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"keywords": ["scheduler", "cron", "jobs", "triggers", "operations"],
|
|
14
|
+
"categories": ["system", "observability"],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/framerslab/agentos-extensions"
|
|
18
|
+
},
|
|
19
|
+
"extensions": [
|
|
20
|
+
{
|
|
21
|
+
"kind": "library",
|
|
22
|
+
"id": "triggers",
|
|
23
|
+
"displayName": "Triggers",
|
|
24
|
+
"description": "A scheduler of record over the storage adapter: definitions registered in code, a lease in the store, runs and missed runs, pause, resume and a hand run, retries and a failed state, a stall watcher",
|
|
25
|
+
"entry": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"requiredSecrets": [],
|
|
29
|
+
"optionalSecrets": [],
|
|
30
|
+
"features": [
|
|
31
|
+
"Three tables under a prefix of the product's own, with migrations on PostgreSQL and SQLite, applied through the adapter's runSchemaMigrations under the product's lock",
|
|
32
|
+
"Schedules of three kinds, at, every and cron (five fields, UTC), with a hardened parser and next-match by epoch arithmetic",
|
|
33
|
+
"One run per due time across processes: a lease claimed in one statement with a token new for each claim, renewed each tick while the run is pending, and a settlement fenced by the token",
|
|
34
|
+
"A missed-run rule a schedule (catch up once, skip, required) applied at start and at resume, each missed due time recorded",
|
|
35
|
+
"Bounded retries with backoff, a failed state that pauses the job for a person, and a hand run for a job marked safe to run twice",
|
|
36
|
+
"A stall watcher for a process that runs no jobs, under its own lease row, that opens one record a stall and closes it when the job runs again",
|
|
37
|
+
"Reports-only rows for jobs that run on their own timer and report their beats here"
|
|
38
|
+
]
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@framers/agentos-ext-triggers",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A scheduler of record in a product's own database through the SQL storage adapter: job rows, a lease with a claim token, runs, missed runs and a missed-run rule a schedule, pause and resume with an actor, a hand run for a job marked safe to run twice, bounded retries and a failed state, a stall watcher, reports-only jobs and an audit hook, under a table prefix of its own",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./manifest.json": "./manifest.json",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"agentos",
|
|
21
|
+
"extension",
|
|
22
|
+
"scheduler",
|
|
23
|
+
"cron",
|
|
24
|
+
"jobs",
|
|
25
|
+
"triggers",
|
|
26
|
+
"operations"
|
|
27
|
+
],
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "Frame",
|
|
30
|
+
"url": "https://frame.dev"
|
|
31
|
+
},
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"manifest.json"
|
|
36
|
+
],
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@framers/sql-storage-adapter": ">=0.7.15"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"@framers/sql-storage-adapter": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@framers/sql-storage-adapter": "^0.9.3",
|
|
47
|
+
"@types/node": "^22.0.0",
|
|
48
|
+
"typescript": "^6.0.3",
|
|
49
|
+
"vitest": "^3.2.7"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22"
|
|
53
|
+
},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/framerslab/agentos-extensions.git",
|
|
57
|
+
"directory": "registry/curated/system/triggers"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/framerslab/agentos-extensions/tree/master/registry/curated/system/triggers#readme",
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/framerslab/agentos-extensions/issues"
|
|
62
|
+
},
|
|
63
|
+
"gitHead": "d4178db7d82adab3cefade4d088a85c53d183818",
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsc --noEmitOnError",
|
|
66
|
+
"dev": "tsc --watch --preserveWatchOutput",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"test:watch": "vitest watch",
|
|
69
|
+
"clean": "rm -rf dist"
|
|
70
|
+
}
|
|
6
71
|
}
|