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
|
@@ -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
|
+
}
|