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,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from 'fs';
4
+ import { pathToFileURL } from 'url';
5
+ import Database from 'better-sqlite3';
6
+ import { resolveSchedulerDbPath } from '../paths.js';
7
+
8
+ function parseArgs(argv) {
9
+ const out = {};
10
+ for (let index = 0; index < argv.length; index += 1) {
11
+ const arg = argv[index];
12
+ if (!arg.startsWith('--')) continue;
13
+ const key = arg.slice(2);
14
+ const next = argv[index + 1];
15
+ if (next && !next.startsWith('--')) {
16
+ out[key] = next;
17
+ index += 1;
18
+ } else {
19
+ out[key] = true;
20
+ }
21
+ }
22
+ return out;
23
+ }
24
+
25
+ function parsePositiveInt(value, fallback) {
26
+ const parsed = Number.parseInt(String(value ?? ''), 10);
27
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
28
+ }
29
+
30
+ function firstNonEmpty(...values) {
31
+ for (const value of values) {
32
+ if (typeof value === 'string' && value.trim()) return value.trim();
33
+ }
34
+ return '';
35
+ }
36
+
37
+ function pickEnvValue(args, key, env, fallback = '') {
38
+ if (args[key]) return args[key];
39
+ const envName = args[`${key}-env`];
40
+ if (envName) return env[envName] || '';
41
+ return fallback;
42
+ }
43
+
44
+ async function fetchTelegram(endpoint, { botToken, method = 'GET', body = null } = {}) {
45
+ const response = await fetch(`https://api.telegram.org/bot${botToken}/${endpoint}`, {
46
+ method,
47
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
48
+ body: body ? JSON.stringify(body) : undefined
49
+ });
50
+ const data = await response.json().catch(() => ({}));
51
+ if (!response.ok || data.ok === false) {
52
+ throw new Error(data.description || `Telegram API request failed (${response.status})`);
53
+ }
54
+ return data.result;
55
+ }
56
+
57
+ async function fetchGatewayHealth(gatewayUrl) {
58
+ if (!gatewayUrl) return null;
59
+ try {
60
+ const response = await fetch(`${gatewayUrl.replace(/\/$/, '')}/health`);
61
+ return { ok: response.ok, status: response.status };
62
+ } catch (err) {
63
+ return { ok: false, error: err.message };
64
+ }
65
+ }
66
+
67
+ function readRecentTelegramFailures(dbPath) {
68
+ if (!dbPath || !existsSync(dbPath)) return null;
69
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
70
+ try {
71
+ const row = db.prepare(`
72
+ SELECT COUNT(*) AS count
73
+ FROM messages
74
+ WHERE last_error IS NOT NULL
75
+ AND created_at >= datetime('now', '-24 hours')
76
+ `).get();
77
+ return row?.count ?? 0;
78
+ } catch {
79
+ return null;
80
+ } finally {
81
+ db.close();
82
+ }
83
+ }
84
+
85
+ export function chooseRepairWebhookUrl(webhookInfo, expectedWebhookUrl = '') {
86
+ const currentUrl = webhookInfo?.url || '';
87
+ return firstNonEmpty(expectedWebhookUrl, currentUrl);
88
+ }
89
+
90
+ export function evaluateWebhookHealth({
91
+ label,
92
+ webhookInfo,
93
+ pendingThreshold = 1,
94
+ requireWebhook = true,
95
+ expectedWebhookUrl = '',
96
+ gatewayHealth = null,
97
+ recentTelegramFailures = null,
98
+ botError = null
99
+ }) {
100
+ const issues = [];
101
+ const warnings = [];
102
+
103
+ if (botError) {
104
+ issues.push(`telegram_api_error=${botError}`);
105
+ }
106
+
107
+ if (!botError) {
108
+ if (requireWebhook && !webhookInfo?.url) {
109
+ issues.push('webhook_missing');
110
+ }
111
+ if (expectedWebhookUrl && webhookInfo?.url && webhookInfo.url !== expectedWebhookUrl) {
112
+ issues.push('webhook_url_mismatch');
113
+ }
114
+ if ((webhookInfo?.pending_update_count || 0) >= pendingThreshold && pendingThreshold > 0) {
115
+ issues.push(`pending_update_count=${webhookInfo.pending_update_count}`);
116
+ }
117
+ if (webhookInfo?.last_error_message) {
118
+ issues.push(`last_error_message=${webhookInfo.last_error_message}`);
119
+ }
120
+ if (webhookInfo?.last_error_date) {
121
+ warnings.push(`last_error_date=${webhookInfo.last_error_date}`);
122
+ }
123
+ }
124
+
125
+ if (gatewayHealth && gatewayHealth.ok === false) {
126
+ warnings.push(`gateway_unreachable=${gatewayHealth.error || gatewayHealth.status || 'unknown'}`);
127
+ }
128
+
129
+ if (typeof recentTelegramFailures === 'number' && recentTelegramFailures > 0) {
130
+ warnings.push(`recent_scheduler_delivery_failures=${recentTelegramFailures}`);
131
+ }
132
+
133
+ const status = issues.length > 0 ? 'ALERT' : warnings.length > 0 ? 'WARN' : 'OK';
134
+ const recommendation = issues.some(issue =>
135
+ issue.startsWith('pending_update_count=') || issue.startsWith('last_error_message=')
136
+ )
137
+ ? 'Consider refreshing the webhook with drop_pending_updates=true if the queue appears stuck.'
138
+ : status === 'OK'
139
+ ? 'No action required.'
140
+ : 'Inspect current webhook and gateway state before taking action.';
141
+
142
+ return {
143
+ label,
144
+ status,
145
+ issues,
146
+ warnings,
147
+ recommendation,
148
+ webhook: webhookInfo ? {
149
+ url: webhookInfo.url || '',
150
+ has_custom_certificate: Boolean(webhookInfo.has_custom_certificate),
151
+ pending_update_count: webhookInfo.pending_update_count || 0,
152
+ last_error_date: webhookInfo.last_error_date || null,
153
+ last_error_message: webhookInfo.last_error_message || null,
154
+ max_connections: webhookInfo.max_connections ?? null,
155
+ ip_address: webhookInfo.ip_address || null
156
+ } : null,
157
+ gateway: gatewayHealth,
158
+ recent_scheduler_delivery_failures: recentTelegramFailures
159
+ };
160
+ }
161
+
162
+ function printResult(result) {
163
+ const issueSummary = result.issues?.length ? ` issues=${result.issues.join(',')}` : '';
164
+ const warningSummary = result.warnings?.length ? ` warnings=${result.warnings.join(',')}` : '';
165
+ process.stdout.write(`STATUS: ${result.status} bot=${result.label}${issueSummary}${warningSummary}\n`);
166
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
167
+ }
168
+
169
+ async function main() {
170
+ const args = parseArgs(process.argv.slice(2));
171
+ const label = args.label || 'telegram-bot';
172
+ const botToken = firstNonEmpty(
173
+ pickEnvValue(args, 'bot-token', process.env),
174
+ process.env.TELEGRAM_BOT_TOKEN
175
+ );
176
+
177
+ if (!botToken) {
178
+ process.stderr.write('[telegram-webhook-check] missing bot token; pass --bot-token, --bot-token-env, or TELEGRAM_BOT_TOKEN\n');
179
+ process.exit(2);
180
+ }
181
+
182
+ const gatewayUrl = firstNonEmpty(args['gateway-url'], process.env.OPENCLAW_GATEWAY_URL);
183
+ const expectedWebhookUrl = firstNonEmpty(
184
+ pickEnvValue(args, 'expected-webhook-url', process.env),
185
+ process.env.TELEGRAM_WEBHOOK_URL
186
+ );
187
+ const schedulerDb = firstNonEmpty(args['scheduler-db'], resolveSchedulerDbPath({ env: process.env }));
188
+ const pendingThreshold = parsePositiveInt(args['pending-threshold'], 1);
189
+ const requireWebhook = args['require-webhook'] !== 'false';
190
+
191
+ if (args.repair === 'drop-pending') {
192
+ // Fix: use deleteWebhook (not setWebhook) -- polling mode has no webhook URL to set.
193
+ // setWebhook would try to re-register a webhook which is wrong in polling mode.
194
+ // drop_pending_updates=false preserves queued messages so polling can drain them.
195
+ const repaired = await fetchTelegram('deleteWebhook', {
196
+ botToken,
197
+ method: 'POST',
198
+ body: {
199
+ drop_pending_updates: false
200
+ }
201
+ });
202
+ printResult({
203
+ label,
204
+ status: 'REPAIRED',
205
+ issues: [],
206
+ warnings: [],
207
+ action: 'drop-pending',
208
+ telegram_response: repaired
209
+ });
210
+ return;
211
+ }
212
+
213
+ let webhookInfo = null;
214
+ let botError = null;
215
+ try {
216
+ webhookInfo = await fetchTelegram('getWebhookInfo', { botToken });
217
+ } catch (err) {
218
+ botError = err.message;
219
+ }
220
+
221
+ const gatewayHealth = await fetchGatewayHealth(gatewayUrl);
222
+ const recentFailures = readRecentTelegramFailures(schedulerDb);
223
+ const result = evaluateWebhookHealth({
224
+ label,
225
+ webhookInfo,
226
+ pendingThreshold,
227
+ requireWebhook,
228
+ expectedWebhookUrl,
229
+ gatewayHealth,
230
+ recentTelegramFailures: recentFailures,
231
+ botError
232
+ });
233
+ printResult(result);
234
+ }
235
+
236
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
237
+ await main();
238
+ }