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/team-adapter.js
ADDED
|
@@ -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
|
+
}
|