openclaw-scheduler 0.2.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.
Files changed (70) hide show
  1. package/AGENTS.md +302 -0
  2. package/BEST-PRACTICES.md +506 -0
  3. package/CHANGELOG.md +82 -0
  4. package/CODE_OF_CONDUCT.md +22 -0
  5. package/CONTEXT.md +26 -0
  6. package/CONTRIBUTING.md +73 -0
  7. package/IMPLEMENTATION_SPEC.md +170 -0
  8. package/INSTALL-ADDITIONAL-HOST.md +333 -0
  9. package/INSTALL-LINUX.md +419 -0
  10. package/INSTALL-WINDOWS.md +305 -0
  11. package/INSTALL.md +364 -0
  12. package/JOB-QUICK-REF.md +222 -0
  13. package/LICENSE +21 -0
  14. package/QUICK-START.md +256 -0
  15. package/README.md +2170 -0
  16. package/SECURITY.md +34 -0
  17. package/UNINSTALL.md +129 -0
  18. package/UPGRADING.md +436 -0
  19. package/agents.js +67 -0
  20. package/approval.js +107 -0
  21. package/backup.js +390 -0
  22. package/bin/openclaw-scheduler.js +138 -0
  23. package/cli.js +1083 -0
  24. package/db.js +122 -0
  25. package/dispatch/529-recovery.mjs +204 -0
  26. package/dispatch/README.md +372 -0
  27. package/dispatch/config.example.json +24 -0
  28. package/dispatch/deliver-watcher.sh +57 -0
  29. package/dispatch/hooks.mjs +171 -0
  30. package/dispatch/index.mjs +1836 -0
  31. package/dispatch/watcher.mjs +1396 -0
  32. package/dispatch-queue.js +112 -0
  33. package/dispatcher-approvals.js +96 -0
  34. package/dispatcher-delivery.js +43 -0
  35. package/dispatcher-maintenance.js +242 -0
  36. package/dispatcher-shell.js +29 -0
  37. package/dispatcher-strategies.js +1280 -0
  38. package/dispatcher-utils.js +81 -0
  39. package/dispatcher.js +855 -0
  40. package/docs/adr-schedule-ownership.md +73 -0
  41. package/docs/gateway-contract.md +904 -0
  42. package/docs/plans/2026-03-09-fix-typescript-types.md +91 -0
  43. package/docs/plans/2026-03-09-test-coverage-gaps.md +83 -0
  44. package/docs/plans/2026-03-10-dispatcher-refactor.md +801 -0
  45. package/docs/trust-architecture.md +266 -0
  46. package/gateway.js +473 -0
  47. package/idempotency.js +119 -0
  48. package/index.d.ts +864 -0
  49. package/index.js +17 -0
  50. package/jobs.js +1224 -0
  51. package/messages.js +357 -0
  52. package/migrate-consolidate.js +694 -0
  53. package/migrate.js +125 -0
  54. package/package.json +130 -0
  55. package/paths.js +79 -0
  56. package/prompt-context.js +94 -0
  57. package/retrieval.js +176 -0
  58. package/runs.js +270 -0
  59. package/scheduler-schema.js +101 -0
  60. package/schema.sql +480 -0
  61. package/scripts/dispatch-cli-utils.mjs +65 -0
  62. package/scripts/inbox-consumer.mjs +288 -0
  63. package/scripts/stuck-detector.sh +18 -0
  64. package/scripts/stuck-run-detector.mjs +333 -0
  65. package/scripts/telegram-webhook-check.mjs +238 -0
  66. package/setup.mjs +724 -0
  67. package/shell-result.js +214 -0
  68. package/task-tracker.js +300 -0
  69. package/team-adapter.js +335 -0
  70. package/v02-runtime.js +599 -0
package/messages.js ADDED
@@ -0,0 +1,357 @@
1
+ // Message queue -- inter-agent communication
2
+ import { randomUUID } from 'crypto';
3
+ import { getDb } from './db.js';
4
+
5
+ // Valid message kinds (extended with typed contract kinds)
6
+ const VALID_KINDS = new Set([
7
+ 'text', 'task', 'result', 'status', 'system', 'spawn',
8
+ 'decision', 'constraint', 'fact', 'preference',
9
+ ]);
10
+
11
+ /**
12
+ * Send a message from one agent to another.
13
+ */
14
+ export function sendMessage(opts) {
15
+ if (!opts.from_agent) throw new Error('from_agent is required');
16
+ if (!opts.to_agent) throw new Error('to_agent is required');
17
+ if (opts.body == null) throw new Error('body is required');
18
+ const db = getDb();
19
+ const id = randomUUID();
20
+ const kind = opts.kind || 'text';
21
+
22
+ if (!VALID_KINDS.has(kind)) {
23
+ throw new Error(`Invalid message kind '${kind}'. Valid: ${[...VALID_KINDS].join(', ')}`);
24
+ }
25
+
26
+ db.prepare(`
27
+ INSERT INTO messages (
28
+ id, from_agent, to_agent, team_id, member_id, task_id, reply_to,
29
+ kind, subject, body, metadata, priority, channel, delivery_to, status,
30
+ expires_at, job_id, run_id, owner, ack_required
31
+ )
32
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)
33
+ `).run(
34
+ id,
35
+ opts.from_agent,
36
+ opts.to_agent,
37
+ opts.team_id || null,
38
+ opts.member_id || null,
39
+ opts.task_id || null,
40
+ opts.reply_to || null,
41
+ kind,
42
+ opts.subject || null,
43
+ opts.body,
44
+ opts.metadata ? JSON.stringify(opts.metadata) : null,
45
+ opts.priority || 0,
46
+ opts.channel || null,
47
+ opts.delivery_to || null,
48
+ opts.expires_at || null,
49
+ opts.job_id || null,
50
+ opts.run_id || null,
51
+ opts.owner || null,
52
+ opts.ack_required ? 1 : 0
53
+ );
54
+
55
+ return getMessage(id);
56
+ }
57
+
58
+ /**
59
+ * Get a message by ID.
60
+ */
61
+ export function getMessage(id) {
62
+ const msg = getDb().prepare('SELECT * FROM messages WHERE id = ?').get(id);
63
+ if (msg && msg.metadata) {
64
+ try { msg.metadata = JSON.parse(msg.metadata); } catch (err) {
65
+ process.stderr.write(`[messages] JSON parse error for metadata: ${err.message}\n`);
66
+ }
67
+ }
68
+ return msg;
69
+ }
70
+
71
+ /**
72
+ * Get pending messages for an agent (inbox), ordered by typed priority then
73
+ * numeric priority then time.
74
+ */
75
+ export function getInbox(agentId, opts = {}) {
76
+ const limit = opts.limit || 50;
77
+ const includeRead = opts.includeRead || false;
78
+ const includeDelivered = opts.includeDelivered || false;
79
+ const teamId = opts.teamId || null;
80
+ const memberId = opts.memberId || null;
81
+ const taskId = opts.taskId || null;
82
+
83
+ // SQLite CASE expression mirrors KIND_PRIORITY map
84
+ const kindOrder = `
85
+ CASE kind
86
+ WHEN 'constraint' THEN 0
87
+ WHEN 'decision' THEN 1
88
+ WHEN 'fact' THEN 2
89
+ WHEN 'task' THEN 3
90
+ WHEN 'preference' THEN 4
91
+ ELSE 5
92
+ END`;
93
+
94
+ const whereParts = ['(to_agent = ? OR to_agent = \'broadcast\')'];
95
+ const whereParams = [agentId];
96
+ if (teamId) {
97
+ whereParts.push('team_id = ?');
98
+ whereParams.push(teamId);
99
+ }
100
+ if (memberId) {
101
+ whereParts.push('(member_id IS NULL OR member_id = ?)');
102
+ whereParams.push(memberId);
103
+ }
104
+ if (taskId) {
105
+ whereParts.push('task_id = ?');
106
+ whereParams.push(taskId);
107
+ }
108
+ const whereSql = whereParts.join(' AND ');
109
+
110
+ if (includeRead) {
111
+ return getDb().prepare(`
112
+ SELECT * FROM messages
113
+ WHERE ${whereSql}
114
+ AND status IN ('pending', 'delivered', 'read')
115
+ ORDER BY ${kindOrder} ASC, priority DESC, created_at ASC
116
+ LIMIT ?
117
+ `).all(...whereParams, limit).map(parseMetadata);
118
+ }
119
+
120
+ if (includeDelivered) {
121
+ return getDb().prepare(`
122
+ SELECT * FROM messages
123
+ WHERE ${whereSql}
124
+ AND status IN ('pending', 'delivered')
125
+ ORDER BY ${kindOrder} ASC, priority DESC, created_at ASC
126
+ LIMIT ?
127
+ `).all(...whereParams, limit).map(parseMetadata);
128
+ }
129
+
130
+ return getDb().prepare(`
131
+ SELECT * FROM messages
132
+ WHERE ${whereSql}
133
+ AND status = 'pending'
134
+ ORDER BY ${kindOrder} ASC, priority DESC, created_at ASC
135
+ LIMIT ?
136
+ `).all(...whereParams, limit).map(parseMetadata);
137
+ }
138
+
139
+ /**
140
+ * Team mailbox query (independent of to_agent).
141
+ */
142
+ export function getTeamMessages(teamId, opts = {}) {
143
+ const limit = opts.limit || 50;
144
+ const includeRead = opts.includeRead || false;
145
+ const memberId = opts.memberId || null;
146
+ const taskId = opts.taskId || null;
147
+
148
+ const where = ['team_id = ?'];
149
+ const params = [teamId];
150
+ if (memberId) {
151
+ where.push('(member_id IS NULL OR member_id = ?)');
152
+ params.push(memberId);
153
+ }
154
+ if (taskId) {
155
+ where.push('task_id = ?');
156
+ params.push(taskId);
157
+ }
158
+
159
+ if (!includeRead) {
160
+ where.push("status IN ('pending', 'delivered')");
161
+ }
162
+
163
+ return getDb().prepare(`
164
+ SELECT * FROM messages
165
+ WHERE ${where.join(' AND ')}
166
+ ORDER BY created_at ASC
167
+ LIMIT ?
168
+ `).all(...params, limit).map(parseMetadata);
169
+ }
170
+
171
+ /**
172
+ * Get messages sent by an agent (outbox).
173
+ */
174
+ export function getOutbox(agentId, limit = 50) {
175
+ return getDb().prepare(`
176
+ SELECT * FROM messages
177
+ WHERE from_agent = ?
178
+ ORDER BY created_at DESC
179
+ LIMIT ?
180
+ `).all(agentId, limit).map(parseMetadata);
181
+ }
182
+
183
+ /**
184
+ * Get thread (message + all replies).
185
+ */
186
+ export function getThread(messageId) {
187
+ return getDb().prepare(`
188
+ SELECT * FROM messages
189
+ WHERE id = ? OR reply_to = ?
190
+ ORDER BY created_at ASC
191
+ `).all(messageId, messageId).map(parseMetadata);
192
+ }
193
+
194
+ /**
195
+ * Mark a message as delivered.
196
+ */
197
+ export function markDelivered(id) {
198
+ const result = getDb().prepare(`
199
+ UPDATE messages SET status = 'delivered', delivered_at = datetime('now')
200
+ WHERE id = ? AND status = 'pending'
201
+ `).run(id);
202
+ if (result.changes > 0) {
203
+ recordMessageAttempt(id, { ok: true, actor: 'dispatcher' });
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Mark a message as read.
209
+ */
210
+ export function markRead(id) {
211
+ const result = getDb().prepare(`
212
+ UPDATE messages
213
+ SET status = 'read',
214
+ read_at = datetime('now'),
215
+ ack_at = COALESCE(ack_at, datetime('now'))
216
+ WHERE id = ? AND status IN ('pending', 'delivered')
217
+ `).run(id);
218
+ if (result.changes > 0) {
219
+ addReceipt(id, 'ack', null, 'agent', null);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Explicit ACK helper (alias for markRead with actor).
225
+ */
226
+ export function ackMessage(id, actor = 'agent', detail = null) {
227
+ const result = getDb().prepare(`
228
+ UPDATE messages
229
+ SET status = CASE WHEN status IN ('pending', 'delivered') THEN 'read' ELSE status END,
230
+ read_at = COALESCE(read_at, datetime('now')),
231
+ ack_at = COALESCE(ack_at, datetime('now'))
232
+ WHERE id = ?
233
+ `).run(id);
234
+ if (result.changes > 0) {
235
+ addReceipt(id, 'ack', null, actor, detail);
236
+ }
237
+ return getMessage(id);
238
+ }
239
+
240
+ /**
241
+ * Mark all pending/delivered messages for an agent as read.
242
+ */
243
+ export function markAllRead(agentId) {
244
+ return getDb().prepare(`
245
+ UPDATE messages
246
+ SET status = 'read',
247
+ read_at = datetime('now'),
248
+ ack_at = COALESCE(ack_at, datetime('now'))
249
+ WHERE (to_agent = ? OR to_agent = 'broadcast') AND status IN ('pending', 'delivered')
250
+ `).run(agentId);
251
+ }
252
+
253
+ /**
254
+ * Get unread count for an agent.
255
+ */
256
+ export function getUnreadCount(agentId) {
257
+ const row = getDb().prepare(`
258
+ SELECT COUNT(*) as cnt FROM messages
259
+ WHERE (to_agent = ? OR to_agent = 'broadcast')
260
+ AND status IN ('pending', 'delivered')
261
+ `).get(agentId);
262
+ return row.cnt;
263
+ }
264
+
265
+ /**
266
+ * Expire old messages past their TTL.
267
+ */
268
+ export function expireMessages() {
269
+ return getDb().prepare(`
270
+ UPDATE messages SET status = 'expired'
271
+ WHERE expires_at IS NOT NULL
272
+ AND expires_at < datetime('now')
273
+ AND status IN ('pending', 'delivered')
274
+ `).run();
275
+ }
276
+
277
+ /**
278
+ * Prune old read/expired/delivered messages.
279
+ * - read/expired/failed: after keepDays (default 30)
280
+ * - delivered: after deliveredKeepDays (default 3) -- delivered means consumed, no longer needed
281
+ * - system kind pending/delivered: after systemKeepDays (default 3) -- failure notifications, not actionable
282
+ */
283
+ export function pruneMessages(keepDays = 30, deliveredKeepDays = 3, systemKeepDays = 3) {
284
+ const db = getDb();
285
+ // Prune read/expired/failed after keepDays
286
+ db.prepare(`
287
+ DELETE FROM messages
288
+ WHERE status IN ('read', 'expired', 'failed')
289
+ AND created_at < datetime('now', '-' || ? || ' days')
290
+ `).run(keepDays);
291
+ // Prune delivered messages after deliveredKeepDays
292
+ db.prepare(`
293
+ DELETE FROM messages
294
+ WHERE status = 'delivered'
295
+ AND created_at < datetime('now', '-' || ? || ' days')
296
+ `).run(deliveredKeepDays);
297
+ // Prune system/result notifications after systemKeepDays regardless of status
298
+ // (runs table is the canonical record; these queue messages are just transient notifications)
299
+ return db.prepare(`
300
+ DELETE FROM messages
301
+ WHERE kind IN ('system', 'result')
302
+ AND created_at < datetime('now', '-' || ? || ' days')
303
+ `).run(systemKeepDays);
304
+ }
305
+
306
+ /**
307
+ * Record a delivery attempt (success or failure) for receipt auditing.
308
+ */
309
+ export function recordMessageAttempt(messageId, opts = {}) {
310
+ const ok = opts.ok !== false;
311
+ const actor = opts.actor || 'system';
312
+ const error = ok ? null : (opts.error || 'Delivery failed');
313
+ const db = getDb();
314
+ const row = db.prepare('SELECT delivery_attempts FROM messages WHERE id = ?').get(messageId);
315
+ if (!row) return null;
316
+ const nextAttempt = (row.delivery_attempts || 0) + 1;
317
+ db.prepare(`
318
+ UPDATE messages
319
+ SET delivery_attempts = COALESCE(delivery_attempts, 0) + 1,
320
+ last_error = ?
321
+ WHERE id = ?
322
+ `).run(error, messageId);
323
+ addReceipt(messageId, ok ? 'attempt' : 'error', nextAttempt, actor, error);
324
+ return getMessage(messageId);
325
+ }
326
+
327
+ /**
328
+ * List receipt events for a message.
329
+ */
330
+ export function listMessageReceipts(messageId, limit = 50) {
331
+ return getDb().prepare(`
332
+ SELECT * FROM message_receipts
333
+ WHERE message_id = ?
334
+ ORDER BY created_at DESC
335
+ LIMIT ?
336
+ `).all(messageId, limit);
337
+ }
338
+
339
+ function addReceipt(messageId, eventType, attempt = null, actor = 'system', detail = null) {
340
+ try {
341
+ getDb().prepare(`
342
+ INSERT INTO message_receipts (id, message_id, event_type, attempt, actor, detail)
343
+ VALUES (?, ?, ?, ?, ?, ?)
344
+ `).run(randomUUID(), messageId, eventType, attempt, actor, detail);
345
+ } catch (err) {
346
+ process.stderr.write(`[messages] receipt insert error: ${err.message}\n`);
347
+ }
348
+ }
349
+
350
+ function parseMetadata(msg) {
351
+ if (msg && msg.metadata && typeof msg.metadata === 'string') {
352
+ try { msg.metadata = JSON.parse(msg.metadata); } catch (err) {
353
+ process.stderr.write(`[messages] JSON parse error for metadata: ${err.message}\n`);
354
+ }
355
+ }
356
+ return msg;
357
+ }