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
@@ -0,0 +1,335 @@
1
+ // Team adapter -- map scheduler queue messages to team mailbox/task events.
2
+ import { randomUUID } from 'crypto';
3
+ import { getDb } from './db.js';
4
+ import { ackMessage } from './messages.js';
5
+ import { createTaskGroup, getTaskGroup, checkGroupCompletion } from './task-tracker.js';
6
+
7
+ function sqliteNow() {
8
+ return new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '');
9
+ }
10
+
11
+ function taskKeyFromMessage(msg) {
12
+ if (msg.task_id) return msg.task_id;
13
+ if (msg.kind === 'task') return msg.id;
14
+ return null;
15
+ }
16
+
17
+ function safeJson(value) {
18
+ try { return JSON.stringify(value); } catch { return null; }
19
+ }
20
+
21
+ export function mapTeamMessages(limit = 100) {
22
+ const db = getDb();
23
+ const msgs = db.prepare(`
24
+ SELECT * FROM messages
25
+ WHERE team_id IS NOT NULL
26
+ AND team_mapped_at IS NULL
27
+ ORDER BY created_at ASC
28
+ LIMIT ?
29
+ `).all(limit);
30
+
31
+ if (msgs.length === 0) return 0;
32
+ const now = sqliteNow();
33
+
34
+ const mapOne = db.transaction((msg) => {
35
+ const taskId = taskKeyFromMessage(msg);
36
+ let eventType = 'mailbox';
37
+
38
+ if (taskId) {
39
+ const existing = db.prepare(`
40
+ SELECT team_id, id
41
+ FROM team_tasks
42
+ WHERE team_id = ? AND id = ?
43
+ `).get(msg.team_id, taskId);
44
+
45
+ if (!existing) {
46
+ db.prepare(`
47
+ INSERT INTO team_tasks (
48
+ team_id, id, member_id, source_message_id, title, status,
49
+ created_at, updated_at
50
+ )
51
+ VALUES (?, ?, ?, ?, ?, 'open', ?, ?)
52
+ `).run(
53
+ msg.team_id,
54
+ taskId,
55
+ msg.member_id || null,
56
+ msg.id,
57
+ msg.subject || (msg.body || '').slice(0, 120) || 'Team task',
58
+ now,
59
+ now
60
+ );
61
+ eventType = 'task_created';
62
+ } else {
63
+ db.prepare(`
64
+ UPDATE team_tasks
65
+ SET member_id = COALESCE(member_id, ?),
66
+ source_message_id = COALESCE(source_message_id, ?),
67
+ updated_at = ?
68
+ WHERE team_id = ? AND id = ?
69
+ `).run(msg.member_id || null, msg.id, now, msg.team_id, taskId);
70
+ eventType = 'task_message';
71
+ }
72
+ }
73
+
74
+ db.prepare(`
75
+ INSERT INTO team_mailbox_events (id, team_id, member_id, task_id, message_id, event_type, payload, created_at)
76
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
77
+ `).run(
78
+ randomUUID(),
79
+ msg.team_id,
80
+ msg.member_id || null,
81
+ taskId,
82
+ msg.id,
83
+ eventType,
84
+ safeJson({
85
+ kind: msg.kind,
86
+ from_agent: msg.from_agent,
87
+ to_agent: msg.to_agent,
88
+ subject: msg.subject,
89
+ status: msg.status,
90
+ priority: msg.priority,
91
+ ack_required: !!msg.ack_required,
92
+ }),
93
+ now
94
+ );
95
+
96
+ db.prepare(`
97
+ UPDATE messages
98
+ SET team_mapped_at = COALESCE(team_mapped_at, ?)
99
+ WHERE id = ?
100
+ `).run(now, msg.id);
101
+ });
102
+
103
+ let mapped = 0;
104
+ for (const msg of msgs) {
105
+ try {
106
+ mapOne(msg);
107
+ mapped++;
108
+ } catch (err) {
109
+ process.stderr.write(`[team-adapter] mapOne error for msg ${msg.id}: ${err?.message || String(err)}
110
+ `);
111
+ }
112
+ }
113
+
114
+ return mapped;
115
+ }
116
+
117
+ export function listTeamTasks(teamId, limit = 50) {
118
+ return getDb().prepare(`
119
+ SELECT *
120
+ FROM team_tasks
121
+ WHERE team_id = ?
122
+ ORDER BY updated_at DESC
123
+ LIMIT ?
124
+ `).all(teamId, limit);
125
+ }
126
+
127
+ export function listTeamMailboxEvents(teamId, opts = {}) {
128
+ const limit = opts.limit || 50;
129
+ const taskId = opts.taskId || null;
130
+ if (taskId) {
131
+ return getDb().prepare(`
132
+ SELECT *
133
+ FROM team_mailbox_events
134
+ WHERE team_id = ? AND task_id = ?
135
+ ORDER BY created_at DESC
136
+ LIMIT ?
137
+ `).all(teamId, taskId, limit);
138
+ }
139
+ return getDb().prepare(`
140
+ SELECT *
141
+ FROM team_mailbox_events
142
+ WHERE team_id = ?
143
+ ORDER BY created_at DESC
144
+ LIMIT ?
145
+ `).all(teamId, limit);
146
+ }
147
+
148
+ export function createTeamTaskGate({
149
+ teamId,
150
+ taskId,
151
+ expectedMembers,
152
+ timeoutS = 600,
153
+ createdBy = 'main',
154
+ deliveryChannel,
155
+ deliveryTo,
156
+ }) {
157
+ if (!teamId) throw new Error('teamId is required');
158
+ if (!taskId) throw new Error('taskId is required');
159
+ if (!Array.isArray(expectedMembers) || expectedMembers.length === 0) {
160
+ throw new Error('expectedMembers must be a non-empty array');
161
+ }
162
+
163
+ const db = getDb();
164
+ const now = sqliteNow();
165
+ const group = createTaskGroup({
166
+ name: `team:${teamId}:task:${taskId}`,
167
+ expectedAgents: expectedMembers,
168
+ timeoutS,
169
+ createdBy,
170
+ deliveryChannel,
171
+ deliveryTo,
172
+ });
173
+
174
+ const upsert = db.prepare(`
175
+ INSERT INTO team_tasks (
176
+ team_id, id, title, status, gate_tracker_id, gate_status, created_at, updated_at
177
+ )
178
+ VALUES (?, ?, ?, 'blocked', ?, 'waiting', ?, ?)
179
+ ON CONFLICT(team_id, id) DO UPDATE SET
180
+ status = 'blocked',
181
+ gate_tracker_id = excluded.gate_tracker_id,
182
+ gate_status = 'waiting',
183
+ updated_at = excluded.updated_at
184
+ `);
185
+ upsert.run(
186
+ teamId,
187
+ taskId,
188
+ `Team task ${taskId}`,
189
+ group.id,
190
+ now,
191
+ now
192
+ );
193
+
194
+ db.prepare(`
195
+ INSERT INTO team_mailbox_events (id, team_id, task_id, event_type, payload, created_at)
196
+ VALUES (?, ?, ?, 'gate_open', ?, ?)
197
+ `).run(
198
+ randomUUID(),
199
+ teamId,
200
+ taskId,
201
+ safeJson({ tracker_id: group.id, expected_members: expectedMembers, timeout_s: timeoutS }),
202
+ now
203
+ );
204
+
205
+ return {
206
+ team_id: teamId,
207
+ task_id: taskId,
208
+ gate_status: 'waiting',
209
+ tracker_id: group.id,
210
+ expected_members: expectedMembers,
211
+ };
212
+ }
213
+
214
+ export function checkTeamTaskGates(limit = 100) {
215
+ const db = getDb();
216
+ const rows = db.prepare(`
217
+ SELECT team_id, id as task_id, gate_tracker_id
218
+ FROM team_tasks
219
+ WHERE gate_status = 'waiting'
220
+ AND gate_tracker_id IS NOT NULL
221
+ ORDER BY updated_at ASC
222
+ LIMIT ?
223
+ `).all(limit);
224
+
225
+ if (rows.length === 0) return { passed: 0, failed: 0, pending: 0 };
226
+ let passed = 0;
227
+ let failed = 0;
228
+ let pending = 0;
229
+ const now = sqliteNow();
230
+
231
+ for (const row of rows) {
232
+ // Ensure tracker terminal state is evaluated before reading status.
233
+ checkGroupCompletion(row.gate_tracker_id);
234
+ const tracker = getTaskGroup(row.gate_tracker_id);
235
+ if (!tracker) {
236
+ failed += 1;
237
+ db.prepare(`
238
+ UPDATE team_tasks
239
+ SET gate_status = 'failed',
240
+ status = 'failed',
241
+ last_error = 'Missing tracker',
242
+ updated_at = ?,
243
+ completed_at = COALESCE(completed_at, ?)
244
+ WHERE team_id = ? AND id = ?
245
+ `).run(now, now, row.team_id, row.task_id);
246
+ db.prepare(`
247
+ INSERT INTO team_mailbox_events (id, team_id, task_id, event_type, payload, created_at)
248
+ VALUES (?, ?, ?, 'gate_failed', ?, ?)
249
+ `).run(randomUUID(), row.team_id, row.task_id, safeJson({ tracker_id: row.gate_tracker_id, reason: 'missing_tracker' }), now);
250
+ continue;
251
+ }
252
+
253
+ if (tracker.status === 'active') {
254
+ pending += 1;
255
+ continue;
256
+ }
257
+
258
+ if (tracker.status === 'completed') {
259
+ passed += 1;
260
+ db.prepare(`
261
+ UPDATE team_tasks
262
+ SET gate_status = 'passed',
263
+ status = 'open',
264
+ updated_at = ?
265
+ WHERE team_id = ? AND id = ?
266
+ `).run(now, row.team_id, row.task_id);
267
+ db.prepare(`
268
+ INSERT INTO team_mailbox_events (id, team_id, task_id, event_type, payload, created_at)
269
+ VALUES (?, ?, ?, 'gate_passed', ?, ?)
270
+ `).run(
271
+ randomUUID(),
272
+ row.team_id,
273
+ row.task_id,
274
+ safeJson({ tracker_id: row.gate_tracker_id, summary: tracker.summary || null }),
275
+ now
276
+ );
277
+ continue;
278
+ }
279
+
280
+ failed += 1;
281
+ db.prepare(`
282
+ UPDATE team_tasks
283
+ SET gate_status = 'failed',
284
+ status = 'failed',
285
+ last_error = ?,
286
+ updated_at = ?,
287
+ completed_at = COALESCE(completed_at, ?)
288
+ WHERE team_id = ? AND id = ?
289
+ `).run(
290
+ tracker.summary || `Gate ${tracker.status}`,
291
+ now,
292
+ now,
293
+ row.team_id,
294
+ row.task_id
295
+ );
296
+ db.prepare(`
297
+ INSERT INTO team_mailbox_events (id, team_id, task_id, event_type, payload, created_at)
298
+ VALUES (?, ?, ?, 'gate_failed', ?, ?)
299
+ `).run(
300
+ randomUUID(),
301
+ row.team_id,
302
+ row.task_id,
303
+ safeJson({ tracker_id: row.gate_tracker_id, summary: tracker.summary || null, status: tracker.status }),
304
+ now
305
+ );
306
+ }
307
+
308
+ return { passed, failed, pending };
309
+ }
310
+
311
+ export function ackTeamMessage(messageId, actor = 'team-member', detail = null) {
312
+ const db = getDb();
313
+ const msg = db.prepare(`
314
+ SELECT *
315
+ FROM messages
316
+ WHERE id = ? AND team_id IS NOT NULL
317
+ `).get(messageId);
318
+ if (!msg) return null;
319
+
320
+ const updated = ackMessage(messageId, actor, detail);
321
+ const taskId = taskKeyFromMessage(msg);
322
+ db.prepare(`
323
+ INSERT INTO team_mailbox_events (id, team_id, member_id, task_id, message_id, event_type, payload, created_at)
324
+ VALUES (?, ?, ?, ?, ?, 'ack', ?, ?)
325
+ `).run(
326
+ randomUUID(),
327
+ msg.team_id,
328
+ msg.member_id || null,
329
+ taskId,
330
+ msg.id,
331
+ safeJson({ actor, detail }),
332
+ sqliteNow()
333
+ );
334
+ return updated;
335
+ }