@hybrd/scheduler 2.0.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/README.md ADDED
@@ -0,0 +1,339 @@
1
+ # @hybrd/scheduler
2
+
3
+ Agentic scheduling system for Hybrid AI agents. **100% OpenClaw-compatible** with additional features for agent-native workflows.
4
+
5
+ ## Overview
6
+
7
+ The scheduler enables agents to schedule future actions for themselves. This is "agentic scheduling" - the LLM decides when and what to schedule, not a human.
8
+
9
+ ## OpenClaw Compatibility
10
+
11
+ This scheduler implements the complete OpenClaw CronService API:
12
+
13
+ ### Schedule Types
14
+
15
+ ```typescript
16
+ type CronSchedule =
17
+ | { kind: "at"; at: string } // One-time at specific time
18
+ | { kind: "every"; everyMs: number; anchorMs?: number } // Interval with anchor
19
+ | { kind: "cron"; expr: string; tz?: string; staggerMs?: number } // Cron expression
20
+ ```
21
+
22
+ ### Core API
23
+
24
+ | Method | Description |
25
+ |--------|-------------|
26
+ | `start()` | Start the scheduler, clear stale markers, arm timer |
27
+ | `stop()` | Stop the scheduler |
28
+ | `status()` | Get scheduler status (enabled, job count, next wake) |
29
+ | `list()` | List all jobs (sorted by next run) |
30
+ | `listPage(opts)` | Paginated list with query, sort, offset |
31
+ | `add(input)` | Create a new scheduled job |
32
+ | `update(id, patch)` | Update job properties |
33
+ | `remove(id)` | Delete a job |
34
+ | `run(id, mode)` | Manually trigger a job |
35
+ | `get(id)` | Get job by ID |
36
+
37
+ ## Architecture
38
+
39
+ ```
40
+ ┌─────────────────────────────────────────────────────────────────┐
41
+ │ SchedulerService │
42
+ ├─────────────────────────────────────────────────────────────────┤
43
+ │ State: │
44
+ │ ├── running: boolean │
45
+ │ ├── timer: setTimeout handle │
46
+ │ └── op: Promise chain (for locking) │
47
+ ├─────────────────────────────────────────────────────────────────┤
48
+ │ Core Flow: │
49
+ │ 1. armTimer() → Set precise timer to next wake time │
50
+ │ 2. onTimer() → Find due jobs, mark running, execute │
51
+ │ 3. executeJob() → Run payload, deliver, apply result │
52
+ │ 4. applyResult() → Update state, compute next run │
53
+ └─────────────────────────────────────────────────────────────────┘
54
+ ```
55
+
56
+ ## Features
57
+
58
+ ### Precise Timer
59
+
60
+ Unlike polling-based schedulers, this implementation arms the timer to the exact next wake time:
61
+
62
+ ```typescript
63
+ private armTimer(): void {
64
+ const nextAt = this.nextWakeAtMs() // Find earliest job
65
+ if (!nextAt) return
66
+
67
+ const delay = Math.max(0, nextAt - Date.now())
68
+ this.state.timer = setTimeout(() => this.onTimer(), delay)
69
+ }
70
+ ```
71
+
72
+ ### Concurrency Protection
73
+
74
+ Uses `runningAtMs` marker to prevent double-execution:
75
+
76
+ ```typescript
77
+ if (typeof job.state.runningAtMs === "number") {
78
+ // Already running - check if stuck (> 2 hours)
79
+ if (Date.now() - job.state.runningAtMs > STUCK_RUN_MS) {
80
+ return true // Stuck, allow re-run
81
+ }
82
+ return false // Still running
83
+ }
84
+ ```
85
+
86
+ ### Error Backoff
87
+
88
+ Exponential backoff on consecutive errors:
89
+
90
+ | Errors | Backoff |
91
+ |--------|---------|
92
+ | 1 | 30 seconds |
93
+ | 2 | 60 seconds |
94
+ | 3 | 5 minutes |
95
+ | 4 | 15 minutes |
96
+ | 5+ | 1 hour |
97
+
98
+ ### Missed Job Catchup
99
+
100
+ On startup, clears stale `runningAtMs` markers (from crashes) and runs any due jobs.
101
+
102
+ ## Job Definition
103
+
104
+ ```typescript
105
+ interface CronJob {
106
+ id: string
107
+ agentId?: string
108
+ sessionKey?: string // For reminder delivery context
109
+ name: string
110
+ description?: string
111
+ enabled: boolean
112
+ deleteAfterRun?: boolean // Auto-delete after successful run
113
+ createdAtMs: number
114
+ updatedAtMs: number
115
+ schedule: CronSchedule
116
+ sessionTarget: "main" | "isolated"
117
+ wakeMode: "now" | "next-heartbeat"
118
+ payload: CronPayload
119
+ delivery?: CronDelivery
120
+ state: CronJobState
121
+ }
122
+ ```
123
+
124
+ ### Payload Types
125
+
126
+ ```typescript
127
+ type CronPayload =
128
+ | { kind: "systemEvent"; text: string }
129
+ | {
130
+ kind: "agentTurn"
131
+ message: string
132
+ model?: string
133
+ thinking?: string
134
+ timeoutSeconds?: number
135
+ allowUnsafeExternalContent?: boolean
136
+ }
137
+ ```
138
+
139
+ ### Delivery Configuration
140
+
141
+ ```typescript
142
+ interface CronDelivery {
143
+ mode: "none" | "announce"
144
+ channel?: string // "xmtp", etc.
145
+ to?: string // Recipient address
146
+ accountId?: string
147
+ bestEffort?: boolean
148
+ }
149
+ ```
150
+
151
+ ## Usage
152
+
153
+ ### Basic Setup
154
+
155
+ ```typescript
156
+ import { SchedulerService, createSqliteStore } from "@hybrd/scheduler"
157
+
158
+ const store = await createSqliteStore({ dbPath: "./data/scheduler.db" })
159
+
160
+ const scheduler = new SchedulerService({
161
+ store,
162
+ dispatcher: channelDispatcher,
163
+ executor: {
164
+ runAgentTurn: async (job) => { /* ... */ },
165
+ runSystemEvent: async (job) => { /* ... */ }
166
+ },
167
+ enabled: true
168
+ })
169
+
170
+ await scheduler.start()
171
+ ```
172
+
173
+ ### Schedule a One-Time Task
174
+
175
+ ```typescript
176
+ await scheduler.add({
177
+ name: "Reminder",
178
+ schedule: { kind: "at", at: "2026-03-01T09:00:00Z" },
179
+ payload: { kind: "agentTurn", message: "Check on the project" },
180
+ delivery: { mode: "announce", channel: "xmtp", to: "0x..." }
181
+ })
182
+ ```
183
+
184
+ ### Schedule an Interval Task
185
+
186
+ ```typescript
187
+ await scheduler.add({
188
+ name: "Status Check",
189
+ schedule: { kind: "every", everyMs: 300000 }, // 5 minutes
190
+ payload: { kind: "agentTurn", message: "Check status" }
191
+ })
192
+ ```
193
+
194
+ ### Schedule a Cron Task
195
+
196
+ ```typescript
197
+ await scheduler.add({
198
+ name: "Daily Standup",
199
+ schedule: { kind: "cron", expr: "0 9 * * 1-5", tz: "America/Chicago" },
200
+ payload: { kind: "agentTurn", message: "Time for standup!" }
201
+ })
202
+ ```
203
+
204
+ ## MCP Tools
205
+
206
+ The scheduler provides ready-to-use MCP tools for agent integration:
207
+
208
+ ```typescript
209
+ import { createSchedulerTools } from "@hybrd/scheduler"
210
+
211
+ const tools = createSchedulerTools(scheduler)
212
+
213
+ // Use with Claude Agent SDK
214
+ const agent = new Agent({
215
+ tools: [...otherTools, ...tools]
216
+ })
217
+ ```
218
+
219
+ ### Available Tools
220
+
221
+ | Tool | Description |
222
+ |------|-------------|
223
+ | `schedule_task` | Create a new scheduled task |
224
+ | `list_tasks` | List tasks with pagination and filtering |
225
+ | `cancel_task` | Cancel and remove a task |
226
+ | `get_task` | Get task details by ID |
227
+ | `run_task` | Manually trigger a task |
228
+
229
+ ## Channel Adapters
230
+
231
+ Scheduler jobs can deliver results via channel adapters:
232
+
233
+ ```typescript
234
+ interface ChannelAdapter {
235
+ readonly channel: ChannelId
236
+ readonly port: number
237
+ start(): Promise<void>
238
+ stop(): Promise<void>
239
+ trigger(req: TriggerRequest): Promise<TriggerResponse>
240
+ }
241
+ ```
242
+
243
+ ### Built-in Adapters
244
+
245
+ | Channel | Port | Description |
246
+ |---------|------|-------------|
247
+ | `xmtp` | 8455 | XMTP messaging |
248
+
249
+ ### Custom Adapter
250
+
251
+ ```typescript
252
+ class SlackAdapter implements ChannelAdapter {
253
+ readonly channel = "slack"
254
+ readonly port = 8457
255
+
256
+ async trigger(req: TriggerRequest): Promise<TriggerResponse> {
257
+ // POST to Slack API
258
+ return { delivered: true, messageId: "..." }
259
+ }
260
+ }
261
+ ```
262
+
263
+ ## Feature Comparison
264
+
265
+ | Feature | OpenClaw | Hybrid |
266
+ |---------|:--------:|:------:|
267
+ | Precise timer | ✅ | ✅ |
268
+ | Concurrency protection | ✅ | ✅ |
269
+ | Missed job catchup | ✅ | ✅ |
270
+ | Error backoff | ✅ | ✅ |
271
+ | Pagination API | ✅ | ✅ |
272
+ | SQLite storage | ❌ | ✅ |
273
+ | MCP tools | ❌ | ✅ |
274
+ | Channel adapters | ❌ | ✅ |
275
+ | XMTP integration | ❌ | ✅ |
276
+
277
+ ## Storage
278
+
279
+ Uses SQLite via sql.js (WASM) for persistence:
280
+
281
+ ```typescript
282
+ interface ScheduledTaskRow {
283
+ id: string
284
+ name: string
285
+ schedule: string // JSON
286
+ payload: string // JSON
287
+ delivery: string // JSON
288
+ state: string // JSON
289
+ enabled: number // 0 or 1
290
+ created_at: number
291
+ updated_at: number
292
+ }
293
+ ```
294
+
295
+ Benefits over JSON file storage:
296
+ - Atomic writes
297
+ - Query support
298
+ - Better performance
299
+ - Works in WASM environments
300
+
301
+ ## Configuration
302
+
303
+ ```typescript
304
+ interface SchedulerConfig {
305
+ store: SqliteSchedulerStore
306
+ dispatcher: ChannelDispatcher
307
+ executor: SchedulerExecutor
308
+ enabled?: boolean // Default: true
309
+ timezone?: string // Default: system timezone
310
+ }
311
+ ```
312
+
313
+ ## Environment Variables
314
+
315
+ | Variable | Default | Description |
316
+ |----------|---------|-------------|
317
+ | `SCHEDULER_ENABLED` | `true` | Enable/disable scheduler |
318
+ | `SCHEDULER_DB_PATH` | `./data/scheduler.db` | SQLite database path |
319
+ | `SCHEDULER_TIMEZONE` | System TZ | Default timezone for cron |
320
+
321
+ ## Events
322
+
323
+ ```typescript
324
+ scheduler.onEvent((event) => {
325
+ // event.action: "added" | "updated" | "removed" | "started" | "finished"
326
+ console.log(`Job ${event.jobId}: ${event.action}`)
327
+ })
328
+ ```
329
+
330
+ ## Testing
331
+
332
+ ```bash
333
+ cd packages/scheduler
334
+ pnpm test
335
+ ```
336
+
337
+ ## License
338
+
339
+ MIT