stonyx 0.2.3-alpha.7 → 0.2.3-alpha.9
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.
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Cron Conventions
|
|
2
|
+
|
|
3
|
+
Scheduling conventions for `@stonyx/cron`. Covers the legacy interval API and the advanced scheduling system.
|
|
4
|
+
|
|
5
|
+
## Legacy API (Simple Intervals)
|
|
6
|
+
|
|
7
|
+
For basic recurring callbacks, use the `Cron` class directly:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import Cron from '@stonyx/cron';
|
|
11
|
+
|
|
12
|
+
const cron = new Cron();
|
|
13
|
+
|
|
14
|
+
// register(key, callback, intervalSeconds, runOnInit?)
|
|
15
|
+
cron.register('health-check', async () => {
|
|
16
|
+
await checkHealth();
|
|
17
|
+
}, 300, true);
|
|
18
|
+
|
|
19
|
+
// Unregister when done
|
|
20
|
+
cron.unregister('health-check');
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`Cron` is a singleton — only one instance per process.
|
|
24
|
+
|
|
25
|
+
## Advanced Scheduling (CronService)
|
|
26
|
+
|
|
27
|
+
For jobs with cron expressions, one-shot scheduling, AI input normalization, and run history:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import CronService from '@stonyx/cron/service';
|
|
31
|
+
|
|
32
|
+
const service = new CronService();
|
|
33
|
+
|
|
34
|
+
service.onJobDue = async (job) => {
|
|
35
|
+
// Execute the job's work
|
|
36
|
+
return { status: 'ok', summary: 'completed' };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
await service.start();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Schedule Kinds
|
|
43
|
+
|
|
44
|
+
Three schedule types, specified in `schedule.kind`:
|
|
45
|
+
|
|
46
|
+
| Kind | Purpose | Required fields |
|
|
47
|
+
|------|---------|----------------|
|
|
48
|
+
| `every` | Recurring interval | `everyMs` (milliseconds) |
|
|
49
|
+
| `cron` | Cron expression | `expr` (5-field), optional `tz` |
|
|
50
|
+
| `at` | One-shot | `at` (ISO-8601 string) |
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// Recurring every 60 seconds
|
|
54
|
+
await service.add({
|
|
55
|
+
name: 'Diagnostics',
|
|
56
|
+
schedule: { kind: 'every', everyMs: 60_000 },
|
|
57
|
+
payload: { kind: 'agentTurn', message: 'run diagnostics' },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Daily at 9am Eastern
|
|
61
|
+
await service.add({
|
|
62
|
+
name: 'Morning Report',
|
|
63
|
+
schedule: { kind: 'cron', expr: '0 9 * * *', tz: 'America/New_York' },
|
|
64
|
+
payload: { kind: 'agentTurn', message: 'generate morning report' },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// One-shot reminder (auto-deletes after run)
|
|
68
|
+
await service.add({
|
|
69
|
+
name: 'Reminder',
|
|
70
|
+
schedule: { kind: 'at', at: '2026-07-01T12:00:00Z' },
|
|
71
|
+
payload: { kind: 'agentTurn', message: 'follow up on PR' },
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Payload Kinds
|
|
76
|
+
|
|
77
|
+
| Kind | Target | Key field |
|
|
78
|
+
|------|--------|-----------|
|
|
79
|
+
| `agentTurn` | Isolated agent session | `message` |
|
|
80
|
+
| `systemEvent` | Main session | `text` |
|
|
81
|
+
|
|
82
|
+
`sessionTarget` is inferred automatically: `agentTurn` → `isolated`, `systemEvent` → `main`.
|
|
83
|
+
|
|
84
|
+
### CRUD
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const job = await service.add({ ... }); // Create
|
|
88
|
+
const found = service.get(job.id); // Read
|
|
89
|
+
const updated = await service.update(job.id, { name: 'Renamed' }); // Update
|
|
90
|
+
await service.remove(job.id); // Delete
|
|
91
|
+
|
|
92
|
+
// List (excludes disabled by default)
|
|
93
|
+
const jobs = service.list();
|
|
94
|
+
const all = service.list({ includeDisabled: true });
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Manual Execution
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// Force-run regardless of schedule
|
|
101
|
+
await service.run(job.id, 'force');
|
|
102
|
+
|
|
103
|
+
// Only run if due
|
|
104
|
+
await service.run(job.id, 'due');
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Run History
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const history = service.runs(job.id);
|
|
111
|
+
// [{ status, error?, summary?, runAtMs, durationMs, nextRunAtMs, ts }]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### AI Input Normalization
|
|
115
|
+
|
|
116
|
+
CronService accepts loose input from AI tool calls and normalizes it:
|
|
117
|
+
|
|
118
|
+
- Missing `schedule.kind` is inferred from fields (`everyMs` → `every`, `expr` → `cron`, `at` → `at`)
|
|
119
|
+
- Bare `message` or `text` at the top level is wrapped into a `payload`
|
|
120
|
+
- `deleteAfterRun` is auto-set for one-shot (`at`) jobs
|
|
121
|
+
- `delivery: { mode: 'announce' }` is auto-set for isolated `agentTurn` jobs
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// AI might send this flat structure
|
|
125
|
+
await service.add({
|
|
126
|
+
name: 'Weather Check',
|
|
127
|
+
schedule: { everyMs: 120000 },
|
|
128
|
+
message: 'check the weather',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Normalized to:
|
|
132
|
+
// {
|
|
133
|
+
// schedule: { kind: 'every', everyMs: 120000 },
|
|
134
|
+
// payload: { kind: 'agentTurn', message: 'check the weather' },
|
|
135
|
+
// sessionTarget: 'isolated',
|
|
136
|
+
// delivery: { mode: 'announce' },
|
|
137
|
+
// }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## ORM Data Model
|
|
141
|
+
|
|
142
|
+
When persisting cron data with `@stonyx/orm`, use the following model structure.
|
|
143
|
+
|
|
144
|
+
### Models
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
models/
|
|
148
|
+
cron-job.js
|
|
149
|
+
cron-job/
|
|
150
|
+
schedule.js
|
|
151
|
+
payload.js
|
|
152
|
+
state.js
|
|
153
|
+
delivery.js
|
|
154
|
+
cron-run.js
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**`cron-job.js`** — parent model with `belongsTo` for schedule, payload, state, and delivery sub-models (property flattening rule — no passthrough objects):
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
import { Model, attr, belongsTo } from '@stonyx/orm';
|
|
161
|
+
|
|
162
|
+
export default class CronJobModel extends Model {
|
|
163
|
+
name = attr('string');
|
|
164
|
+
description = attr('string');
|
|
165
|
+
enabled = attr('boolean');
|
|
166
|
+
deleteAfterRun = attr('boolean');
|
|
167
|
+
sessionTarget = attr('string');
|
|
168
|
+
wakeMode = attr('string');
|
|
169
|
+
createdAtMs = attr('number');
|
|
170
|
+
updatedAtMs = attr('number');
|
|
171
|
+
|
|
172
|
+
schedule = belongsTo('cron-job/schedule');
|
|
173
|
+
payload = belongsTo('cron-job/payload');
|
|
174
|
+
state = belongsTo('cron-job/state');
|
|
175
|
+
delivery = belongsTo('cron-job/delivery');
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**`cron-job/schedule.js`**
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
import { Model, attr } from '@stonyx/orm';
|
|
183
|
+
|
|
184
|
+
export default class CronJobScheduleModel extends Model {
|
|
185
|
+
kind = attr('string');
|
|
186
|
+
at = attr('string');
|
|
187
|
+
everyMs = attr('number');
|
|
188
|
+
anchorMs = attr('number');
|
|
189
|
+
expr = attr('string');
|
|
190
|
+
tz = attr('string');
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**`cron-job/payload.js`**
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
import { Model, attr } from '@stonyx/orm';
|
|
198
|
+
|
|
199
|
+
export default class CronJobPayloadModel extends Model {
|
|
200
|
+
kind = attr('string');
|
|
201
|
+
message = attr('string');
|
|
202
|
+
text = attr('string');
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**`cron-job/state.js`**
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
import { Model, attr } from '@stonyx/orm';
|
|
210
|
+
|
|
211
|
+
export default class CronJobStateModel extends Model {
|
|
212
|
+
nextRunAtMs = attr('number');
|
|
213
|
+
runningAtMs = attr('number');
|
|
214
|
+
lastRunAtMs = attr('number');
|
|
215
|
+
lastStatus = attr('string');
|
|
216
|
+
lastError = attr('string');
|
|
217
|
+
lastDurationMs = attr('number');
|
|
218
|
+
consecutiveErrors = attr('number');
|
|
219
|
+
scheduleErrorCount = attr('number');
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**`cron-job/delivery.js`**
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
import { Model, attr } from '@stonyx/orm';
|
|
227
|
+
|
|
228
|
+
export default class CronJobDeliveryModel extends Model {
|
|
229
|
+
mode = attr('string');
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**`cron-run.js`**
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
import { Model, attr } from '@stonyx/orm';
|
|
237
|
+
|
|
238
|
+
export default class CronRunModel extends Model {
|
|
239
|
+
jobId = attr('string');
|
|
240
|
+
status = attr('string');
|
|
241
|
+
error = attr('string');
|
|
242
|
+
summary = attr('string');
|
|
243
|
+
runAtMs = attr('number');
|
|
244
|
+
durationMs = attr('number');
|
|
245
|
+
nextRunAtMs = attr('number');
|
|
246
|
+
ts = attr('number');
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Property ordering:** `attr()` → `belongsTo()` (on parent model).
|
|
251
|
+
|
|
252
|
+
### DB Schema
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
import { Model, hasMany } from '@stonyx/orm';
|
|
256
|
+
|
|
257
|
+
export default class DBModel extends Model {
|
|
258
|
+
cronJobs = hasMany('cron-job');
|
|
259
|
+
cronJobSchedules = hasMany('cron-job/schedule');
|
|
260
|
+
cronJobPayloads = hasMany('cron-job/payload');
|
|
261
|
+
cronJobStates = hasMany('cron-job/state');
|
|
262
|
+
cronJobDeliveries = hasMany('cron-job/delivery');
|
|
263
|
+
cronRuns = hasMany('cron-run');
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Configuration
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
// config/environment.js
|
|
271
|
+
export default {
|
|
272
|
+
cron: {
|
|
273
|
+
log: true, // enable cron logging (uses stonyx/log)
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## When to Use Which
|
|
279
|
+
|
|
280
|
+
- **Simple recurring callback** → `Cron.register(key, callback, interval)`
|
|
281
|
+
- **Scheduled jobs with history, CRUD, AI input** → `CronService`
|
|
282
|
+
- **Never use raw `setInterval` or `setTimeout`** for recurring work
|
|
@@ -86,9 +86,22 @@ All pub/sub event handling. Never create custom event emitters.
|
|
|
86
86
|
|
|
87
87
|
All scheduled and interval tasks. Never use raw `setInterval` or `setTimeout` for recurring work.
|
|
88
88
|
|
|
89
|
+
**Legacy API** — simple recurring callbacks:
|
|
90
|
+
|
|
89
91
|
- `register(key, callback, interval, runOnInit?)` — schedule a recurring job
|
|
90
92
|
- `unregister(key)` — cancel a job
|
|
91
93
|
|
|
94
|
+
**Advanced API** (`@stonyx/cron/service`) — full scheduling with CRUD, run history, and AI normalization:
|
|
95
|
+
|
|
96
|
+
- `add(input)` / `get(id)` / `update(id, patch)` / `remove(id)` / `list(opts?)` — CRUD
|
|
97
|
+
- `run(id, mode?)` — manual execution (`'force'` or `'due'`)
|
|
98
|
+
- `runs(id, limit?)` — run history
|
|
99
|
+
- `onJobDue` — callback for job execution
|
|
100
|
+
|
|
101
|
+
Three schedule kinds: `every` (interval), `cron` (expression), `at` (one-shot).
|
|
102
|
+
|
|
103
|
+
See [Cron Conventions](./cron-conventions.md) for full details.
|
|
104
|
+
|
|
92
105
|
Configurable via `config/environment.js`:
|
|
93
106
|
|
|
94
107
|
```js
|
|
@@ -18,6 +18,7 @@ Universal rules that apply to every Stonyx project. Section-specific conventions
|
|
|
18
18
|
|
|
19
19
|
- [Project Structure](./project-structure.md) — directory layout, file organization, config conventions
|
|
20
20
|
- [Framework Modules](./framework-modules.md) — when to use which `@stonyx/*` module
|
|
21
|
+
- [Cron Conventions](./cron-conventions.md) — scheduling, job model, CronService API, ORM data model
|
|
21
22
|
- [ORM Conventions](./orm-conventions.md) — models, serializers, access control, transforms, hooks
|
|
22
23
|
- [REST Conventions](./rest-conventions.md) — REST server request classes and handlers
|
|
23
24
|
- [Discord Conventions](./discord-conventions.md) — Discord bot commands and event handlers
|