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.
- package/AGENTS.md +302 -0
- package/BEST-PRACTICES.md +506 -0
- package/CHANGELOG.md +82 -0
- package/CODE_OF_CONDUCT.md +22 -0
- package/CONTEXT.md +26 -0
- package/CONTRIBUTING.md +73 -0
- package/IMPLEMENTATION_SPEC.md +170 -0
- package/INSTALL-ADDITIONAL-HOST.md +333 -0
- package/INSTALL-LINUX.md +419 -0
- package/INSTALL-WINDOWS.md +305 -0
- package/INSTALL.md +364 -0
- package/JOB-QUICK-REF.md +222 -0
- package/LICENSE +21 -0
- package/QUICK-START.md +256 -0
- package/README.md +2170 -0
- package/SECURITY.md +34 -0
- package/UNINSTALL.md +129 -0
- package/UPGRADING.md +436 -0
- package/agents.js +67 -0
- package/approval.js +107 -0
- package/backup.js +390 -0
- package/bin/openclaw-scheduler.js +138 -0
- package/cli.js +1083 -0
- package/db.js +122 -0
- package/dispatch/529-recovery.mjs +204 -0
- package/dispatch/README.md +372 -0
- package/dispatch/config.example.json +24 -0
- package/dispatch/deliver-watcher.sh +57 -0
- package/dispatch/hooks.mjs +171 -0
- package/dispatch/index.mjs +1836 -0
- package/dispatch/watcher.mjs +1396 -0
- package/dispatch-queue.js +112 -0
- package/dispatcher-approvals.js +96 -0
- package/dispatcher-delivery.js +43 -0
- package/dispatcher-maintenance.js +242 -0
- package/dispatcher-shell.js +29 -0
- package/dispatcher-strategies.js +1280 -0
- package/dispatcher-utils.js +81 -0
- package/dispatcher.js +855 -0
- package/docs/adr-schedule-ownership.md +73 -0
- package/docs/gateway-contract.md +904 -0
- package/docs/plans/2026-03-09-fix-typescript-types.md +91 -0
- package/docs/plans/2026-03-09-test-coverage-gaps.md +83 -0
- package/docs/plans/2026-03-10-dispatcher-refactor.md +801 -0
- package/docs/trust-architecture.md +266 -0
- package/gateway.js +473 -0
- package/idempotency.js +119 -0
- package/index.d.ts +864 -0
- package/index.js +17 -0
- package/jobs.js +1224 -0
- package/messages.js +357 -0
- package/migrate-consolidate.js +694 -0
- package/migrate.js +125 -0
- package/package.json +130 -0
- package/paths.js +79 -0
- package/prompt-context.js +94 -0
- package/retrieval.js +176 -0
- package/runs.js +270 -0
- package/scheduler-schema.js +101 -0
- package/schema.sql +480 -0
- package/scripts/dispatch-cli-utils.mjs +65 -0
- package/scripts/inbox-consumer.mjs +288 -0
- package/scripts/stuck-detector.sh +18 -0
- package/scripts/stuck-run-detector.mjs +333 -0
- package/scripts/telegram-webhook-check.mjs +238 -0
- package/setup.mjs +724 -0
- package/shell-result.js +214 -0
- package/task-tracker.js +300 -0
- package/team-adapter.js +335 -0
- 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
|
+
}
|