openclaw-scheduler 0.2.8 → 0.2.10
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/CHANGELOG.md +1 -0
- package/INSTALL-ADDITIONAL-HOST.md +1 -1
- package/INSTALL-LINUX.md +1 -1
- package/INSTALL-WINDOWS.md +1 -1
- package/INSTALL.md +1 -1
- package/JOB-QUICK-REF.md +2 -0
- package/README.md +5 -5
- package/cli.js +9 -1
- package/dispatch/529-recovery.mjs +21 -2
- package/dispatch/completion.mjs +49 -0
- package/dispatch/index.mjs +179 -11
- package/dispatch/paths.mjs +36 -0
- package/dispatch/watcher.mjs +78 -9
- package/dispatcher-strategies.js +121 -72
- package/dispatcher.js +4 -2
- package/docs/gateway-contract.md +21 -0
- package/gateway.js +140 -30
- package/index.d.ts +5 -0
- package/jobs.js +23 -8
- package/migrate-consolidate.js +6 -2
- package/package.json +4 -3
- package/paths.js +43 -1
- package/scheduler-schema.js +2 -0
- package/schema.sql +6 -1
- package/setup.mjs +24 -22
package/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface JobSpec {
|
|
|
33
33
|
payload_kind?: 'systemEvent' | 'agentTurn' | 'shellCommand';
|
|
34
34
|
payload_message: string;
|
|
35
35
|
payload_model?: string | null;
|
|
36
|
+
payload_model_fallback?: string | null;
|
|
36
37
|
payload_thinking?: string | null;
|
|
37
38
|
payload_timeout_seconds?: number;
|
|
38
39
|
payload_scope?: 'own' | 'global';
|
|
@@ -87,6 +88,7 @@ export interface JobSpec {
|
|
|
87
88
|
|
|
88
89
|
// Auth profile override
|
|
89
90
|
auth_profile?: string | null;
|
|
91
|
+
auth_profile_fallback?: string | null;
|
|
90
92
|
|
|
91
93
|
// Delivery opt-out
|
|
92
94
|
delivery_opt_out_reason?: string | null;
|
|
@@ -150,6 +152,8 @@ export interface JobRecord extends JobSpec {
|
|
|
150
152
|
schedule_cron: string | null;
|
|
151
153
|
schedule_at: string | null;
|
|
152
154
|
schedule_tz: string;
|
|
155
|
+
payload_model_fallback?: string | null;
|
|
156
|
+
auth_profile_fallback?: string | null;
|
|
153
157
|
payload_kind: 'systemEvent' | 'agentTurn' | 'shellCommand';
|
|
154
158
|
payload_message: string;
|
|
155
159
|
ttl_hours: number | null;
|
|
@@ -455,6 +459,7 @@ export interface AgentTurnWithTimeoutOpts {
|
|
|
455
459
|
sessionKey?: string;
|
|
456
460
|
model?: string;
|
|
457
461
|
authProfile?: string | null;
|
|
462
|
+
sessionKinds?: string[];
|
|
458
463
|
idleTimeoutMs?: number;
|
|
459
464
|
pollIntervalMs?: number;
|
|
460
465
|
absoluteTimeoutMs?: number;
|
package/jobs.js
CHANGED
|
@@ -35,11 +35,11 @@ function sqliteNow(offsetMs = 0) {
|
|
|
35
35
|
|
|
36
36
|
const PATCHABLE_COLUMNS = new Set([
|
|
37
37
|
'enabled', 'name', 'schedule_cron', 'schedule_tz', 'schedule_at', 'schedule_kind',
|
|
38
|
-
'next_run_at', 'last_run_at', 'last_status', 'payload_message', 'payload_model',
|
|
38
|
+
'next_run_at', 'last_run_at', 'last_status', 'payload_message', 'payload_model', 'payload_model_fallback',
|
|
39
39
|
'payload_thinking', 'payload_timeout_seconds', 'session_target', 'run_timeout_ms',
|
|
40
40
|
'max_retries', 'consecutive_errors',
|
|
41
41
|
'delivery_mode', 'delivery_channel', 'delivery_to', 'delivery_opt_out_reason',
|
|
42
|
-
'delete_after_run', 'ttl_hours', 'auth_profile', 'origin',
|
|
42
|
+
'delete_after_run', 'ttl_hours', 'auth_profile', 'auth_profile_fallback', 'origin',
|
|
43
43
|
'output_excerpt_limit_bytes', 'output_summary_limit_bytes',
|
|
44
44
|
'watchdog_check_cmd', 'watchdog_timeout_min', 'watchdog_started_at',
|
|
45
45
|
'watchdog_target_label', 'watchdog_alert_channel', 'watchdog_alert_target',
|
|
@@ -192,9 +192,11 @@ export function validateJobSpec(opts, currentJob = null, mode = 'create') {
|
|
|
192
192
|
'resource_pool',
|
|
193
193
|
'preferred_session_key',
|
|
194
194
|
'payload_model',
|
|
195
|
+
'payload_model_fallback',
|
|
195
196
|
'payload_thinking',
|
|
196
197
|
'trigger_condition',
|
|
197
198
|
'auth_profile',
|
|
199
|
+
'auth_profile_fallback',
|
|
198
200
|
'delivery_opt_out_reason',
|
|
199
201
|
'origin',
|
|
200
202
|
// v0.2 nullable strings
|
|
@@ -397,6 +399,9 @@ export function validateJobSpec(opts, currentJob = null, mode = 'create') {
|
|
|
397
399
|
if (mode === 'create' || 'payload_model' in normalized) {
|
|
398
400
|
assertSafeString('payload_model', merged.payload_model, { allowEmpty: false, maxLength: 256 });
|
|
399
401
|
}
|
|
402
|
+
if (mode === 'create' || 'payload_model_fallback' in normalized) {
|
|
403
|
+
assertSafeString('payload_model_fallback', merged.payload_model_fallback, { allowEmpty: false, maxLength: 256 });
|
|
404
|
+
}
|
|
400
405
|
if (mode === 'create' || 'payload_thinking' in normalized) {
|
|
401
406
|
assertSafeString('payload_thinking', merged.payload_thinking, { allowEmpty: false, maxLength: 64 });
|
|
402
407
|
}
|
|
@@ -408,6 +413,14 @@ export function validateJobSpec(opts, currentJob = null, mode = 'create') {
|
|
|
408
413
|
assertSafeString('auth_profile', merged.auth_profile, { allowEmpty: false, maxLength: 256 });
|
|
409
414
|
}
|
|
410
415
|
}
|
|
416
|
+
if (mode === 'create' || 'auth_profile_fallback' in normalized) {
|
|
417
|
+
if (merged.auth_profile_fallback != null) {
|
|
418
|
+
if (typeof merged.auth_profile_fallback !== 'string') {
|
|
419
|
+
throw new Error('auth_profile_fallback must be a string or null');
|
|
420
|
+
}
|
|
421
|
+
assertSafeString('auth_profile_fallback', merged.auth_profile_fallback, { allowEmpty: false, maxLength: 256 });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
411
424
|
|
|
412
425
|
// Origin tracking (v20): required on creation for root (non-child) jobs.
|
|
413
426
|
// Format convention: "<channel>:<id>" e.g. "telegram:<your-user-id>", "telegram:<your-group-id>", or "system" for automated jobs.
|
|
@@ -651,7 +664,7 @@ export function createJob(opts) {
|
|
|
651
664
|
INSERT INTO jobs (
|
|
652
665
|
id, name, enabled, schedule_kind, schedule_at, schedule_cron, schedule_tz,
|
|
653
666
|
session_target, agent_id, payload_kind, payload_message,
|
|
654
|
-
payload_model, payload_thinking, payload_timeout_seconds,
|
|
667
|
+
payload_model, payload_model_fallback, payload_thinking, payload_timeout_seconds,
|
|
655
668
|
execution_intent, execution_read_only,
|
|
656
669
|
overlap_policy, run_timeout_ms, max_queued_dispatches, max_pending_approvals, max_trigger_fanout,
|
|
657
670
|
delivery_mode, delivery_channel, delivery_to,
|
|
@@ -668,7 +681,7 @@ export function createJob(opts) {
|
|
|
668
681
|
watchdog_timeout_min, watchdog_alert_channel, watchdog_alert_target,
|
|
669
682
|
watchdog_self_destruct, watchdog_started_at,
|
|
670
683
|
ttl_hours,
|
|
671
|
-
auth_profile,
|
|
684
|
+
auth_profile, auth_profile_fallback,
|
|
672
685
|
delivery_opt_out_reason,
|
|
673
686
|
origin,
|
|
674
687
|
identity_principal, identity_run_as, identity_attestation, identity_ref,
|
|
@@ -684,7 +697,7 @@ export function createJob(opts) {
|
|
|
684
697
|
) VALUES (
|
|
685
698
|
?, ?, ?, ?, ?, ?, ?,
|
|
686
699
|
?, ?, ?, ?,
|
|
687
|
-
?, ?, ?,
|
|
700
|
+
?, ?, ?, ?,
|
|
688
701
|
?, ?,
|
|
689
702
|
?, ?, ?, ?, ?,
|
|
690
703
|
?, ?, ?,
|
|
@@ -698,7 +711,7 @@ export function createJob(opts) {
|
|
|
698
711
|
?, ?, ?,
|
|
699
712
|
?, ?, ?,
|
|
700
713
|
?, ?,
|
|
701
|
-
?,
|
|
714
|
+
?, ?,
|
|
702
715
|
?,
|
|
703
716
|
?,
|
|
704
717
|
?,
|
|
@@ -728,6 +741,7 @@ export function createJob(opts) {
|
|
|
728
741
|
finalKind,
|
|
729
742
|
normalized.payload_message,
|
|
730
743
|
normalized.payload_model || null,
|
|
744
|
+
normalized.payload_model_fallback || null,
|
|
731
745
|
normalized.payload_thinking || null,
|
|
732
746
|
normalized.payload_timeout_seconds ?? 120,
|
|
733
747
|
normalized.execution_intent || 'execute',
|
|
@@ -771,6 +785,7 @@ export function createJob(opts) {
|
|
|
771
785
|
normalized.watchdog_started_at || null,
|
|
772
786
|
normalized.ttl_hours || null,
|
|
773
787
|
normalized.auth_profile || null,
|
|
788
|
+
normalized.auth_profile_fallback || null,
|
|
774
789
|
normalized.delivery_opt_out_reason || null,
|
|
775
790
|
normalized.origin || null,
|
|
776
791
|
normalized.identity_principal || null,
|
|
@@ -830,7 +845,7 @@ export function updateJob(id, patch) {
|
|
|
830
845
|
const allowed = [
|
|
831
846
|
'name', 'enabled', 'schedule_kind', 'schedule_at', 'schedule_cron', 'schedule_tz',
|
|
832
847
|
'session_target', 'agent_id', 'payload_kind', 'payload_message',
|
|
833
|
-
'payload_model', 'payload_thinking', 'payload_timeout_seconds',
|
|
848
|
+
'payload_model', 'payload_model_fallback', 'payload_thinking', 'payload_timeout_seconds',
|
|
834
849
|
'execution_intent', 'execution_read_only',
|
|
835
850
|
'overlap_policy', 'run_timeout_ms', 'max_queued_dispatches', 'max_pending_approvals', 'max_trigger_fanout',
|
|
836
851
|
'delivery_mode', 'delivery_channel', 'delivery_to',
|
|
@@ -846,7 +861,7 @@ export function updateJob(id, patch) {
|
|
|
846
861
|
'watchdog_timeout_min', 'watchdog_alert_channel', 'watchdog_alert_target',
|
|
847
862
|
'watchdog_self_destruct', 'watchdog_started_at',
|
|
848
863
|
'ttl_hours',
|
|
849
|
-
'auth_profile',
|
|
864
|
+
'auth_profile', 'auth_profile_fallback',
|
|
850
865
|
'delivery_opt_out_reason',
|
|
851
866
|
'origin',
|
|
852
867
|
// v0.2 fields
|
package/migrate-consolidate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* migrate-consolidate.js -- Single idempotent migration for existing databases
|
|
3
3
|
*
|
|
4
|
-
* Brings any DB from any prior version up to the current schema (
|
|
4
|
+
* Brings any DB from any prior version up to the current schema (v24).
|
|
5
5
|
* Fresh installs get everything from schema.sql directly -- this only
|
|
6
6
|
* runs ALTER TABLEs needed for DBs created before the current schema.
|
|
7
7
|
*
|
|
@@ -58,6 +58,7 @@ export default function migrateConsolidate() {
|
|
|
58
58
|
'max_trigger_fanout', 'output_store_limit_bytes',
|
|
59
59
|
'output_excerpt_limit_bytes', 'output_summary_limit_bytes',
|
|
60
60
|
'output_offload_threshold_bytes', 'ttl_hours', 'auth_profile',
|
|
61
|
+
'payload_model_fallback', 'auth_profile_fallback',
|
|
61
62
|
'schedule_kind', 'schedule_at', 'delivery_channel', 'delivery_to',
|
|
62
63
|
'delivery_opt_out_reason', 'origin', 'parent_id', 'created_at',
|
|
63
64
|
'updated_at', 'delete_after_run', 'next_run_at', 'last_run_at',
|
|
@@ -137,7 +138,7 @@ export default function migrateConsolidate() {
|
|
|
137
138
|
`).get()?.cnt ?? 0)
|
|
138
139
|
: 0;
|
|
139
140
|
if (
|
|
140
|
-
current >=
|
|
141
|
+
current >= 24
|
|
141
142
|
&& hasLatestColumns
|
|
142
143
|
&& legacyAtIsoCount === 0
|
|
143
144
|
&& legacyPayloadMismatchCount === 0
|
|
@@ -345,6 +346,9 @@ export default function migrateConsolidate() {
|
|
|
345
346
|
`ALTER TABLE runs ADD COLUMN credential_handoff_summary TEXT DEFAULT NULL`,
|
|
346
347
|
// v23: child credential policy
|
|
347
348
|
`ALTER TABLE jobs ADD COLUMN child_credential_policy TEXT DEFAULT NULL`,
|
|
349
|
+
// v24: explicit fallback model/auth selection
|
|
350
|
+
`ALTER TABLE jobs ADD COLUMN payload_model_fallback TEXT`,
|
|
351
|
+
`ALTER TABLE jobs ADD COLUMN auth_profile_fallback TEXT DEFAULT NULL`,
|
|
348
352
|
];
|
|
349
353
|
|
|
350
354
|
for (const sql of alters) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-scheduler",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "SQLite-backed job scheduler and workflow engine for OpenClaw agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"./package.json": "./package.json"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": "
|
|
20
|
+
"node": "22.x || 24.x || 26.x"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"start": "node dispatcher.js",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"dispatch/index.mjs",
|
|
45
45
|
"dispatch/liveness.mjs",
|
|
46
46
|
"dispatch/message-input.mjs",
|
|
47
|
+
"dispatch/paths.mjs",
|
|
47
48
|
"dispatch/README.md",
|
|
48
49
|
"dispatch/watcher.mjs",
|
|
49
50
|
"scripts/dispatch-cli-utils.mjs",
|
|
@@ -122,7 +123,7 @@
|
|
|
122
123
|
},
|
|
123
124
|
"homepage": "https://github.com/amittell/openclaw-scheduler#readme",
|
|
124
125
|
"dependencies": {
|
|
125
|
-
"better-sqlite3": "^
|
|
126
|
+
"better-sqlite3": "^12.10.0",
|
|
126
127
|
"croner": "^10.0.1"
|
|
127
128
|
},
|
|
128
129
|
"devDependencies": {
|
package/paths.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, mkdirSync } from 'fs';
|
|
2
|
-
import { homedir } from 'os';
|
|
2
|
+
import { homedir, tmpdir } from 'os';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
|
|
@@ -25,6 +25,17 @@ function isNodeModulesInstall(moduleDir) {
|
|
|
25
25
|
return /[\\/]node_modules[\\/](?:@[^\\/]+[\\/])?openclaw-scheduler(?:[\\/]|$)/.test(moduleDir);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function isUsableWorkingDirectory(dirPath) {
|
|
29
|
+
const candidate = firstNonEmpty(dirPath);
|
|
30
|
+
if (!candidate) return false;
|
|
31
|
+
try {
|
|
32
|
+
accessSync(candidate, constants.R_OK | constants.X_OK);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export function resolveSchedulerHome(env = process.env) {
|
|
29
40
|
const explicitHome = firstNonEmpty(env.SCHEDULER_HOME);
|
|
30
41
|
if (explicitHome) return explicitHome;
|
|
@@ -62,6 +73,37 @@ export function resolveBackupStagingDir(env = process.env) {
|
|
|
62
73
|
return join(resolveSchedulerHome(env), '.backup-staging');
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
export function resolveServiceWorkingDirectory(params = {}) {
|
|
77
|
+
const env = params.env || process.env;
|
|
78
|
+
const explicitPath = firstNonEmpty(params.explicitPath);
|
|
79
|
+
if (explicitPath) {
|
|
80
|
+
try {
|
|
81
|
+
mkdirSync(explicitPath, { recursive: true });
|
|
82
|
+
if (isUsableWorkingDirectory(explicitPath)) return explicitPath;
|
|
83
|
+
} catch {
|
|
84
|
+
// Fall through to install-root/scheduler-home heuristics.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const moduleDir = firstNonEmpty(params.moduleDir) || __dirname;
|
|
89
|
+
if (!isNodeModulesInstall(moduleDir) && isUsableWorkingDirectory(moduleDir)) {
|
|
90
|
+
return moduleDir;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const schedulerHome = resolveSchedulerHome(env);
|
|
94
|
+
try {
|
|
95
|
+
mkdirSync(schedulerHome, { recursive: true });
|
|
96
|
+
if (isUsableWorkingDirectory(schedulerHome)) return schedulerHome;
|
|
97
|
+
} catch {
|
|
98
|
+
// Fall through to other safe directories.
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const home = firstNonEmpty(env.HOME) || homedir();
|
|
102
|
+
if (isUsableWorkingDirectory(home)) return home;
|
|
103
|
+
|
|
104
|
+
return tmpdir();
|
|
105
|
+
}
|
|
106
|
+
|
|
65
107
|
export function resolveArtifactsDir(params = {}) {
|
|
66
108
|
const env = params.env || process.env;
|
|
67
109
|
const explicit = firstNonEmpty(params.explicitPath) || firstNonEmpty(env.SCHEDULER_ARTIFACTS_DIR);
|
package/scheduler-schema.js
CHANGED
|
@@ -11,6 +11,7 @@ export const SCHEDULER_SCHEMAS = {
|
|
|
11
11
|
payload_kind: { type: 'string', enum: ['systemEvent', 'agentTurn', 'shellCommand'] },
|
|
12
12
|
payload_message: { type: 'string', maxLength: 100000 },
|
|
13
13
|
payload_model: { type: 'string', nullable: true },
|
|
14
|
+
payload_model_fallback: { type: 'string', nullable: true, description: 'Optional fallback model override for a same-run retry after primary selection failure' },
|
|
14
15
|
payload_thinking: { type: 'string', nullable: true },
|
|
15
16
|
payload_timeout_seconds: { type: 'integer', min: 1, default: 120 },
|
|
16
17
|
execution_intent: { type: 'string', enum: ['execute', 'plan'], default: 'execute' },
|
|
@@ -45,6 +46,7 @@ export const SCHEDULER_SCHEMAS = {
|
|
|
45
46
|
output_offload_threshold_bytes: { type: 'integer', min: 128, default: 65536 },
|
|
46
47
|
preferred_session_key: { type: 'string', nullable: true },
|
|
47
48
|
auth_profile: { type: 'string', nullable: true, description: 'Auth profile override: null=default, "inherit"=main session profile, or "provider:label"' },
|
|
49
|
+
auth_profile_fallback: { type: 'string', nullable: true, description: 'Optional fallback auth profile for a same-run retry after primary selection failure' },
|
|
48
50
|
delivery_opt_out_reason: { type: 'string', nullable: true, maxLength: 256 },
|
|
49
51
|
delete_after_run: { type: 'boolean', default: false },
|
|
50
52
|
run_now: { type: 'boolean', default: false, note: 'create-time convenience flag' },
|
package/schema.sql
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
-- OpenClaw Scheduler Schema (current: v1.7.0, schema version:
|
|
1
|
+
-- OpenClaw Scheduler Schema (current: v1.7.0, schema version: 24)
|
|
2
2
|
-- Full standalone scheduler + message router
|
|
3
3
|
|
|
4
4
|
-- ============================================================
|
|
@@ -23,6 +23,7 @@ CREATE TABLE IF NOT EXISTS jobs (
|
|
|
23
23
|
payload_kind TEXT NOT NULL, -- 'systemEvent' | 'agentTurn' | 'shellCommand'
|
|
24
24
|
payload_message TEXT NOT NULL,
|
|
25
25
|
payload_model TEXT,
|
|
26
|
+
payload_model_fallback TEXT,
|
|
26
27
|
payload_thinking TEXT,
|
|
27
28
|
payload_timeout_seconds INTEGER DEFAULT 120,
|
|
28
29
|
execution_intent TEXT NOT NULL DEFAULT 'execute', -- 'execute' | 'plan'
|
|
@@ -91,6 +92,9 @@ CREATE TABLE IF NOT EXISTS jobs (
|
|
|
91
92
|
-- Auth profile override (v16)
|
|
92
93
|
auth_profile TEXT DEFAULT NULL, -- null=default, 'inherit'=main session profile, or 'provider:label'
|
|
93
94
|
|
|
95
|
+
-- Fallback selection overrides (v24)
|
|
96
|
+
auth_profile_fallback TEXT DEFAULT NULL, -- optional fallback auth profile used after primary selection failure
|
|
97
|
+
|
|
94
98
|
-- Delivery opt-out (v19)
|
|
95
99
|
delivery_opt_out_reason TEXT DEFAULT NULL, -- set when delivery_mode='none' to explicitly skip delivery
|
|
96
100
|
|
|
@@ -478,3 +482,4 @@ INSERT OR IGNORE INTO schema_migrations (version) VALUES (20);
|
|
|
478
482
|
INSERT OR IGNORE INTO schema_migrations (version) VALUES (21);
|
|
479
483
|
INSERT OR IGNORE INTO schema_migrations (version) VALUES (22);
|
|
480
484
|
INSERT OR IGNORE INTO schema_migrations (version) VALUES (23);
|
|
485
|
+
INSERT OR IGNORE INTO schema_migrations (version) VALUES (24);
|
package/setup.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import os from 'os';
|
|
|
19
19
|
import { execSync } from 'child_process';
|
|
20
20
|
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
|
-
import { ensureSchedulerDbParent, resolveSchedulerDbPath } from './paths.js';
|
|
22
|
+
import { ensureSchedulerDbParent, resolveSchedulerDbPath, resolveServiceWorkingDirectory } from './paths.js';
|
|
23
23
|
import { createJob } from './jobs.js';
|
|
24
24
|
import { initDb } from './db.js';
|
|
25
25
|
|
|
@@ -152,7 +152,8 @@ print();
|
|
|
152
152
|
// --- Step 1: Paths ------------------------------------------------------------
|
|
153
153
|
|
|
154
154
|
print('-- Step 1: Paths ---------------------------------------');
|
|
155
|
-
const
|
|
155
|
+
const schedulerInstallRoot = __dirname;
|
|
156
|
+
const serviceWorkingDirectory = resolveServiceWorkingDirectory({ env: process.env, moduleDir: schedulerInstallRoot });
|
|
156
157
|
const defaultWorkspace = path.join(os.homedir(), '.openclaw', 'workspace');
|
|
157
158
|
const workspacePath = await ask('Workspace path', defaultWorkspace);
|
|
158
159
|
const defaultGateway = 'http://127.0.0.1:18789';
|
|
@@ -162,10 +163,11 @@ const schedulerDbPath = resolveSchedulerDbPath({ env: process.env });
|
|
|
162
163
|
if (schedulerDbPath !== ':memory:') ensureSchedulerDbParent(schedulerDbPath);
|
|
163
164
|
|
|
164
165
|
print();
|
|
165
|
-
print(` Scheduler:
|
|
166
|
-
print(`
|
|
167
|
-
print(`
|
|
168
|
-
print(`
|
|
166
|
+
print(` Scheduler install root: ${schedulerInstallRoot}`);
|
|
167
|
+
print(` Service working dir: ${serviceWorkingDirectory}`);
|
|
168
|
+
print(` Workspace: ${workspacePath}`);
|
|
169
|
+
print(` Gateway: ${gatewayUrl}`);
|
|
170
|
+
print(` Deliver to: ${deliverTo || '(none -- skipping job creation)'}`);
|
|
169
171
|
print();
|
|
170
172
|
|
|
171
173
|
// --- Preflight: npm install behavior -----------------------------------------
|
|
@@ -193,9 +195,9 @@ print();
|
|
|
193
195
|
|
|
194
196
|
print('-- Step 2: Database migrations -------------------------');
|
|
195
197
|
try {
|
|
196
|
-
const { setDbPath } = await import(path.join(
|
|
198
|
+
const { setDbPath } = await import(path.join(schedulerInstallRoot, 'db.js'));
|
|
197
199
|
setDbPath(schedulerDbPath);
|
|
198
|
-
const migrate = (await import(path.join(
|
|
200
|
+
const migrate = (await import(path.join(schedulerInstallRoot, 'migrate-consolidate.js'))).default;
|
|
199
201
|
const ran = migrate();
|
|
200
202
|
if (ran) {
|
|
201
203
|
ok(`Migrations applied -> ${schedulerDbPath}`);
|
|
@@ -213,9 +215,9 @@ print();
|
|
|
213
215
|
print('-- Step 3: Agent memory files --------------------------');
|
|
214
216
|
|
|
215
217
|
const memoryMd = path.join(workspacePath, 'MEMORY.md');
|
|
216
|
-
const memoryEntry = `- **Scheduler Queue Pattern:** Use \`node ${
|
|
217
|
-
Inbox Consumer (\`${
|
|
218
|
-
Stuck Run Detector (\`${
|
|
218
|
+
const memoryEntry = `- **Scheduler Queue Pattern:** Use \`node ${schedulerInstallRoot}/cli.js msg send <from> <to> "body"\` for signal-only queue entries.
|
|
219
|
+
Inbox Consumer (\`${schedulerInstallRoot}/scripts/inbox-consumer.mjs\`) drains pending queue messages to Telegram.
|
|
220
|
+
Stuck Run Detector (\`${schedulerInstallRoot}/scripts/stuck-run-detector.mjs\`) alerts on stale \`running\` runs.`;
|
|
219
221
|
|
|
220
222
|
const memResult = appendIfMissing(memoryMd, 'Scheduler Queue Pattern', memoryEntry);
|
|
221
223
|
if (memResult === true) ok('Appended scheduler queue entry -> MEMORY.md');
|
|
@@ -228,10 +230,10 @@ const indexSection = `### Scheduler & Dispatch
|
|
|
228
230
|
|
|
229
231
|
| File | Covers | Load |
|
|
230
232
|
|------|--------|------|
|
|
231
|
-
| \`${
|
|
232
|
-
| \`${
|
|
233
|
-
| \`${
|
|
234
|
-
| \`${
|
|
233
|
+
| \`${schedulerInstallRoot}/\` | Standalone SQLite scheduler. CLI: \`node cli.js\`. launchd service: \`ai.openclaw.scheduler\`. | Any scheduler/cron work |
|
|
234
|
+
| \`${schedulerInstallRoot}/cli.js\` | Queue + run operations: \`msg send\`, \`msg inbox\`, \`runs running\`, \`runs stale\`. | Day-to-day scheduler operations |
|
|
235
|
+
| \`${schedulerInstallRoot}/scripts/inbox-consumer.mjs\` | Drains queue messages for one agent and delivers to Telegram. | Queue/inbox consumption |
|
|
236
|
+
| \`${schedulerInstallRoot}/scripts/stuck-run-detector.mjs\` | Detects stale \`running\` runs and exits non-zero for alerts. | Run health monitoring |`;
|
|
235
237
|
|
|
236
238
|
// Try inserting before a common section header, fall back to append.
|
|
237
239
|
// NOTE: the link emoji anchors must match the actual markdown heading in
|
|
@@ -278,7 +280,7 @@ if (!deliverTo) {
|
|
|
278
280
|
const existingNames = listJobs().map(r => r.name);
|
|
279
281
|
|
|
280
282
|
// Inbox Consumer
|
|
281
|
-
const icScript = path.join(
|
|
283
|
+
const icScript = path.join(schedulerInstallRoot, 'scripts', 'inbox-consumer.mjs');
|
|
282
284
|
const icName = 'Inbox Consumer';
|
|
283
285
|
if (existingNames.includes(icName)) {
|
|
284
286
|
skip(`"${icName}" job already exists`);
|
|
@@ -304,7 +306,7 @@ if (!deliverTo) {
|
|
|
304
306
|
|
|
305
307
|
// Stuck Run Detector
|
|
306
308
|
const srdName = 'Stuck Run Detector';
|
|
307
|
-
const srdScript = path.join(
|
|
309
|
+
const srdScript = path.join(schedulerInstallRoot, 'scripts', 'stuck-run-detector.mjs');
|
|
308
310
|
const srdCmd = `node ${srdScript} --threshold-min 45`; // coding tasks regularly take 30m+
|
|
309
311
|
if (existingNames.includes(srdName)) {
|
|
310
312
|
skip(`"${srdName}" job already exists`);
|
|
@@ -338,7 +340,7 @@ print();
|
|
|
338
340
|
|
|
339
341
|
const platform = process.platform;
|
|
340
342
|
const nodePath = process.execPath;
|
|
341
|
-
const indexPath = path.join(
|
|
343
|
+
const indexPath = path.join(schedulerInstallRoot, 'dispatcher.js');
|
|
342
344
|
const logPath = platform === 'win32'
|
|
343
345
|
? path.join(os.tmpdir(), 'openclaw-scheduler.log')
|
|
344
346
|
: '/tmp/openclaw-scheduler.log';
|
|
@@ -457,7 +459,7 @@ if (platform === 'darwin') {
|
|
|
457
459
|
<string>${xmlEscape(indexPath)}</string>
|
|
458
460
|
</array>
|
|
459
461
|
${userXml} <key>WorkingDirectory</key>
|
|
460
|
-
<string>${xmlEscape(
|
|
462
|
+
<string>${xmlEscape(serviceWorkingDirectory)}</string>
|
|
461
463
|
<key>EnvironmentVariables</key>
|
|
462
464
|
<dict>
|
|
463
465
|
<key>HOME</key>
|
|
@@ -565,7 +567,7 @@ After=network.target
|
|
|
565
567
|
|
|
566
568
|
[Service]
|
|
567
569
|
Type=simple
|
|
568
|
-
WorkingDirectory=${
|
|
570
|
+
WorkingDirectory=${serviceWorkingDirectory}
|
|
569
571
|
ExecStart=${nodePath} --no-warnings ${indexPath}
|
|
570
572
|
Environment=OPENCLAW_GATEWAY_URL=${gatewayUrl}${gatewayToken ? `\nEnvironment="OPENCLAW_GATEWAY_TOKEN=${gatewayToken.replace(/"/g, '\\"')}"` : ''}
|
|
571
573
|
Environment=SCHEDULER_DB=${schedulerDbPath}
|
|
@@ -612,7 +614,7 @@ WantedBy=default.target
|
|
|
612
614
|
if (install) {
|
|
613
615
|
try {
|
|
614
616
|
execSync(
|
|
615
|
-
`pm2 start "${indexPath}" --name "${pm2Name}" --cwd "${
|
|
617
|
+
`pm2 start "${indexPath}" --name "${pm2Name}" --cwd "${serviceWorkingDirectory}" ` +
|
|
616
618
|
`--log "${logPath}"`,
|
|
617
619
|
{
|
|
618
620
|
stdio: 'inherit',
|
|
@@ -652,7 +654,7 @@ WantedBy=default.target
|
|
|
652
654
|
print(' Setup steps:');
|
|
653
655
|
print(' 1. Install WSL2: wsl --install (in PowerShell as Admin)');
|
|
654
656
|
print(' 2. Open your WSL terminal and run this wizard again from there:');
|
|
655
|
-
print(` cd ${
|
|
657
|
+
print(` cd ${schedulerInstallRoot.replace(/\\/g, '/')}`);
|
|
656
658
|
print(' node setup.mjs');
|
|
657
659
|
print();
|
|
658
660
|
print(' WSL2 with systemd enabled gives the best experience (auto-start on login).');
|