openclaw-scheduler 0.2.9 → 0.2.11
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 +7 -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 +50 -0
- package/dispatch/index.mjs +179 -11
- package/dispatch/watcher.mjs +106 -16
- 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 +3 -3
- package/paths.js +43 -1
- package/scheduler-schema.js +2 -0
- package/schema.sql +6 -1
- package/setup.mjs +24 -22
package/gateway.js
CHANGED
|
@@ -144,6 +144,7 @@ export async function runAgentTurn(opts) {
|
|
|
144
144
|
* @param {number} opts.pollIntervalMs - How often to poll session activity (default: 60000)
|
|
145
145
|
* @param {number} opts.absoluteTimeoutMs - Hard ceiling regardless of activity (default: 300000)
|
|
146
146
|
* @param {string} opts.authProfile - Auth profile override (null, 'inherit', or 'provider:label')
|
|
147
|
+
* @param {string[]} [opts.sessionKinds] - Optional session kinds to track for activity polling
|
|
147
148
|
*/
|
|
148
149
|
export async function runAgentTurnWithActivityTimeout(opts) {
|
|
149
150
|
const {
|
|
@@ -155,10 +156,46 @@ export async function runAgentTurnWithActivityTimeout(opts) {
|
|
|
155
156
|
idleTimeoutMs = 120000, // per-check idle threshold (from payload_timeout_seconds)
|
|
156
157
|
pollIntervalMs = 60000, // check activity every 60s
|
|
157
158
|
absoluteTimeoutMs = 300000, // hard ceiling (run_timeout_ms)
|
|
159
|
+
sessionKinds,
|
|
158
160
|
} = opts;
|
|
159
161
|
|
|
160
162
|
const controller = new AbortController();
|
|
161
163
|
let abortReason = null;
|
|
164
|
+
const normalizedAgentId = (agentId || 'main').toLowerCase();
|
|
165
|
+
const normalizedSessionKey = String(sessionKey || '').toLowerCase();
|
|
166
|
+
|
|
167
|
+
const inferSessionKinds = () => {
|
|
168
|
+
if (Array.isArray(sessionKinds) && sessionKinds.length > 0) {
|
|
169
|
+
return [...new Set(sessionKinds.map(k => String(k).toLowerCase()).filter(Boolean))];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Explicitly isolated/subagent sessions should not be pinned to main session
|
|
173
|
+
// so they can report idleness based on their own active session records.
|
|
174
|
+
if (
|
|
175
|
+
normalizedSessionKey === 'isolated' ||
|
|
176
|
+
normalizedSessionKey.startsWith('isolated:') ||
|
|
177
|
+
normalizedSessionKey.endsWith(':isolated') ||
|
|
178
|
+
normalizedSessionKey.includes(':isolated:') ||
|
|
179
|
+
normalizedAgentId === 'subagent'
|
|
180
|
+
) {
|
|
181
|
+
return ['subagent', 'isolated'];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Default to including main unless we can clearly infer this is an isolated run.
|
|
185
|
+
if (
|
|
186
|
+
normalizedAgentId === 'main' ||
|
|
187
|
+
normalizedSessionKey === 'main' ||
|
|
188
|
+
normalizedSessionKey.startsWith('main:') ||
|
|
189
|
+
normalizedSessionKey.includes(':main:') ||
|
|
190
|
+
normalizedSessionKey.endsWith(':main')
|
|
191
|
+
) {
|
|
192
|
+
return ['main', 'subagent', 'isolated'];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return ['main', 'subagent', 'isolated'];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const resolvedSessionKinds = inferSessionKinds();
|
|
162
199
|
|
|
163
200
|
// Hard absolute ceiling -- always fires regardless of activity
|
|
164
201
|
const absoluteTimer = setTimeout(() => {
|
|
@@ -171,7 +208,7 @@ export async function runAgentTurnWithActivityTimeout(opts) {
|
|
|
171
208
|
|
|
172
209
|
const checkActivity = async () => {
|
|
173
210
|
try {
|
|
174
|
-
const result = await listSessions({ kinds:
|
|
211
|
+
const result = await listSessions({ kinds: resolvedSessionKinds, activeMinutes: 60 });
|
|
175
212
|
// Normalise: gateway wraps result in several layers
|
|
176
213
|
const sessions =
|
|
177
214
|
result?.result?.details?.sessions ||
|
|
@@ -551,20 +588,55 @@ export async function waitForGateway(timeoutMs = 30000, intervalMs = 2000) {
|
|
|
551
588
|
* @param {string} [agentId='main'] - Agent ID for store path resolution
|
|
552
589
|
* @returns {{ ok: boolean, error?: string }}
|
|
553
590
|
*/
|
|
554
|
-
|
|
555
|
-
if (!sessionKey || !authProfile) {
|
|
556
|
-
return { ok: false, error: 'sessionKey and authProfile are required' };
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// The gateway may persist session state under either the canonical agent-scoped
|
|
560
|
-
// key or the flat transport key, depending on which path created the session.
|
|
561
|
-
// Keep both aliases in sync so isolated scheduler jobs cannot miss the override.
|
|
591
|
+
function resolveSessionKeyAliases(sessionKey, agentId = 'main') {
|
|
562
592
|
const canonicalMatch = sessionKey.match(/^agent:[^:]+:(.+)$/);
|
|
563
593
|
const canonicalKey = sessionKey.startsWith('agent:')
|
|
564
594
|
? sessionKey
|
|
565
595
|
: `agent:${agentId}:${sessionKey}`;
|
|
566
596
|
const flatSessionKey = canonicalMatch?.[1] || sessionKey;
|
|
567
|
-
|
|
597
|
+
return Array.from(new Set([canonicalKey, flatSessionKey]));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function parseSessionModelRef(modelRef) {
|
|
601
|
+
const trimmed = typeof modelRef === 'string' ? modelRef.trim() : '';
|
|
602
|
+
if (!trimmed) {
|
|
603
|
+
return { providerOverride: undefined, modelOverride: undefined };
|
|
604
|
+
}
|
|
605
|
+
const slashIndex = trimmed.indexOf('/');
|
|
606
|
+
if (slashIndex <= 0 || slashIndex >= trimmed.length - 1) {
|
|
607
|
+
return { providerOverride: undefined, modelOverride: trimmed };
|
|
608
|
+
}
|
|
609
|
+
const providerOverride = trimmed.slice(0, slashIndex).trim();
|
|
610
|
+
const modelOverride = trimmed.slice(slashIndex + 1).trim();
|
|
611
|
+
return {
|
|
612
|
+
providerOverride: providerOverride || undefined,
|
|
613
|
+
modelOverride: modelOverride || undefined,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Write scheduler-managed session overrides directly to the gateway's sessions.json store.
|
|
619
|
+
*
|
|
620
|
+
* The gateway reads sessions.json on each agent turn (with mtime-based cache
|
|
621
|
+
* invalidation), so writing here before dispatch ensures the embedded runner
|
|
622
|
+
* picks up the correct auth profile and model selection.
|
|
623
|
+
*
|
|
624
|
+
* @param {string} sessionKey - Session key as used in the HTTP request (e.g. 'scheduler:<jobId>')
|
|
625
|
+
* @param {{ authProfile?: string | null, modelRef?: string | null }} overrides - Desired session overrides
|
|
626
|
+
* @param {string} [agentId='main'] - Agent ID for store path resolution
|
|
627
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
628
|
+
*/
|
|
629
|
+
export function applySessionOverridesToSessionStore(sessionKey, overrides = {}, agentId = 'main') {
|
|
630
|
+
if (!sessionKey) {
|
|
631
|
+
return { ok: false, error: 'sessionKey is required' };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const authProfile = typeof overrides.authProfile === 'string' ? overrides.authProfile.trim() : '';
|
|
635
|
+
const shouldSetAuthProfile = Boolean(authProfile) && authProfile !== 'inherit';
|
|
636
|
+
const { providerOverride, modelOverride } = parseSessionModelRef(overrides.modelRef);
|
|
637
|
+
const shouldSetModelOverride = Boolean(modelOverride);
|
|
638
|
+
|
|
639
|
+
const keyAliases = resolveSessionKeyAliases(sessionKey, agentId);
|
|
568
640
|
const sessionsPath = join(HOME_DIR, '.openclaw', 'agents', agentId, 'sessions', 'sessions.json');
|
|
569
641
|
|
|
570
642
|
try {
|
|
@@ -579,28 +651,59 @@ export function applyAuthProfileToSessionStore(sessionKey, authProfile, agentId
|
|
|
579
651
|
let changed = false;
|
|
580
652
|
|
|
581
653
|
for (const key of keyAliases) {
|
|
582
|
-
const
|
|
583
|
-
if (!
|
|
584
|
-
// Session doesn't exist yet -- create a minimal entry.
|
|
585
|
-
// The gateway will populate the rest on the first agent turn.
|
|
586
|
-
store[key] = {
|
|
587
|
-
updatedAt: now,
|
|
588
|
-
authProfileOverride: authProfile,
|
|
589
|
-
authProfileOverrideSource: 'user',
|
|
590
|
-
};
|
|
591
|
-
changed = true;
|
|
654
|
+
const existingEntry = store[key];
|
|
655
|
+
if (!existingEntry && !shouldSetAuthProfile && !shouldSetModelOverride) {
|
|
592
656
|
continue;
|
|
593
657
|
}
|
|
594
658
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
entry.
|
|
600
|
-
|
|
659
|
+
const entry = existingEntry || { updatedAt: now };
|
|
660
|
+
let entryChanged = false;
|
|
661
|
+
|
|
662
|
+
if (shouldSetAuthProfile) {
|
|
663
|
+
if (entry.authProfileOverride !== authProfile || entry.authProfileOverrideSource !== 'user') {
|
|
664
|
+
entry.authProfileOverride = authProfile;
|
|
665
|
+
entry.authProfileOverrideSource = 'user';
|
|
666
|
+
delete entry.authProfileOverrideCompactionCount;
|
|
667
|
+
entryChanged = true;
|
|
668
|
+
}
|
|
669
|
+
} else if (
|
|
670
|
+
entry.authProfileOverride !== undefined ||
|
|
671
|
+
entry.authProfileOverrideSource !== undefined ||
|
|
672
|
+
entry.authProfileOverrideCompactionCount !== undefined
|
|
673
|
+
) {
|
|
674
|
+
delete entry.authProfileOverride;
|
|
675
|
+
delete entry.authProfileOverrideSource;
|
|
601
676
|
delete entry.authProfileOverrideCompactionCount;
|
|
602
|
-
|
|
677
|
+
entryChanged = true;
|
|
603
678
|
}
|
|
679
|
+
|
|
680
|
+
if (shouldSetModelOverride) {
|
|
681
|
+
if (entry.modelOverride !== modelOverride) {
|
|
682
|
+
entry.modelOverride = modelOverride;
|
|
683
|
+
entryChanged = true;
|
|
684
|
+
}
|
|
685
|
+
if (providerOverride) {
|
|
686
|
+
if (entry.providerOverride !== providerOverride) {
|
|
687
|
+
entry.providerOverride = providerOverride;
|
|
688
|
+
entryChanged = true;
|
|
689
|
+
}
|
|
690
|
+
} else if (entry.providerOverride !== undefined) {
|
|
691
|
+
delete entry.providerOverride;
|
|
692
|
+
entryChanged = true;
|
|
693
|
+
}
|
|
694
|
+
} else if (entry.modelOverride !== undefined || entry.providerOverride !== undefined) {
|
|
695
|
+
delete entry.modelOverride;
|
|
696
|
+
delete entry.providerOverride;
|
|
697
|
+
entryChanged = true;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (!entryChanged) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
entry.updatedAt = now;
|
|
705
|
+
store[key] = entry;
|
|
706
|
+
changed = true;
|
|
604
707
|
}
|
|
605
708
|
|
|
606
709
|
if (!changed) {
|
|
@@ -614,9 +717,16 @@ export function applyAuthProfileToSessionStore(sessionKey, authProfile, agentId
|
|
|
614
717
|
}
|
|
615
718
|
}
|
|
616
719
|
|
|
720
|
+
export function applyAuthProfileToSessionStore(sessionKey, authProfile, agentId = 'main') {
|
|
721
|
+
if (!sessionKey || !authProfile) {
|
|
722
|
+
return { ok: false, error: 'sessionKey and authProfile are required' };
|
|
723
|
+
}
|
|
724
|
+
return applySessionOverridesToSessionStore(sessionKey, { authProfile }, agentId);
|
|
725
|
+
}
|
|
726
|
+
|
|
617
727
|
/**
|
|
618
|
-
* Sync the live auth-profiles.json from
|
|
619
|
-
*
|
|
728
|
+
* Sync the live auth-profiles.json from the main agent store to the target
|
|
729
|
+
* agent store at ~/.openclaw/agents/<agentId>/agent/auth-profiles.json.
|
|
620
730
|
*
|
|
621
731
|
* This ensures scheduler sessions always use fresh credentials (tokens, order,
|
|
622
732
|
* default profile) even when no explicit auth_profile is set on the job.
|
|
@@ -630,7 +740,7 @@ export function applyAuthProfileToSessionStore(sessionKey, authProfile, agentId
|
|
|
630
740
|
* @returns {{ ok: boolean, error?: string }}
|
|
631
741
|
*/
|
|
632
742
|
export function syncAuthStoreToSession(agentId = 'main') {
|
|
633
|
-
const livePath = join(HOME_DIR, '.openclaw', '
|
|
743
|
+
const livePath = join(HOME_DIR, '.openclaw', 'agents', 'main', 'agent', 'auth-profiles.json');
|
|
634
744
|
const agentStorePath = join(HOME_DIR, '.openclaw', 'agents', agentId, 'agent', 'auth-profiles.json');
|
|
635
745
|
|
|
636
746
|
try {
|
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.11",
|
|
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",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
},
|
|
124
124
|
"homepage": "https://github.com/amittell/openclaw-scheduler#readme",
|
|
125
125
|
"dependencies": {
|
|
126
|
-
"better-sqlite3": "^
|
|
126
|
+
"better-sqlite3": "^12.10.0",
|
|
127
127
|
"croner": "^10.0.1"
|
|
128
128
|
},
|
|
129
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);
|