openclaw-scheduler 0.2.0 → 0.2.1
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 +18 -1
- package/dispatcher-delivery.js +11 -4
- package/dispatcher-strategies.js +10 -3
- package/jobs.js +30 -1
- package/package.json +1 -1
- package/scheduler-schema.js +5 -3
- package/schema.sql +2 -2
- package/scripts/inbox-consumer.mjs +1 -1
- package/shell-result.js +21 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,21 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.2.1] -- 2026-04-01
|
|
6
6
|
|
|
7
7
|
### Fixed
|
|
8
8
|
- fix(watcher): exit cleanly when session status=done (PR #1)
|
|
9
9
|
- fix(watchdog): prevent auto-resolving active sessions with heartbeat + hard ceiling (PR #2)
|
|
10
10
|
- fix(gateway): reset idle timer while fetch is in flight (PR #3)
|
|
11
11
|
- fix(watcher): prevent premature kill of active subagent sessions with JSONL activity signal (PR #7)
|
|
12
|
+
- fix(db): add SQLite busy_timeout (5s) to prevent SQLITE_BUSY on CLI + dispatcher contention
|
|
13
|
+
- fix(approvals): prevent double-dispatch race on auto-approved jobs
|
|
14
|
+
- fix(watcher): cap deadline extension at min(timeout, 4h) to prevent zombie watchers
|
|
15
|
+
- fix(runs): preserve empty string summary/error_message (use ?? instead of ||)
|
|
16
|
+
- fix(runs): guard getTimedOutRuns against NULL run_timeout_ms on legacy rows
|
|
17
|
+
- fix(gateway): use byte length for Telegram message chunking (4096-byte limit)
|
|
18
|
+
- fix(jobs): validate schedule_tz as real IANA timezone via Intl.DateTimeFormat
|
|
19
|
+
- fix(dispatcher): wrap delete_after_run cleanup in transaction
|
|
20
|
+
- fix(dispatch): remove 4000-char truncation in formatMessageForDelivery
|
|
21
|
+
- fix(dispatch): add retry exception path delivery announcement
|
|
22
|
+
- fix(dispatch): fix dispatch CLI subcommand routing in bin wrapper
|
|
12
23
|
|
|
13
24
|
### Added
|
|
14
25
|
- feat: v0.2 runtime with identity/trust/authorization/evidence/credential handoff (PR #4)
|
|
15
26
|
- feat: x-openclaw-env-inject header for agent task credentials (PR #5)
|
|
27
|
+
- feat: [IMAGE:path] marker protocol for shell job image attachments
|
|
28
|
+
- feat: auto-delete watcher and watchdog jobs after completion (delete_after_run)
|
|
29
|
+
- feat: enforce delivery_to as required field on job INSERT
|
|
30
|
+
- feat: multi-platform CI (Linux, macOS, Windows)
|
|
16
31
|
- docs: trust architecture, multi-agent gateway routing, agent adoption files
|
|
32
|
+
- docs: AGENTS.md, CONTEXT.md, JOB-QUICK-REF.md for agent adoption
|
|
17
33
|
|
|
18
34
|
### Changed
|
|
19
35
|
- chore: replace non-ASCII characters with ASCII equivalents (PR #6)
|
|
36
|
+
- chore: bump output_excerpt_limit and output_summary_limit defaults to 64KB
|
|
20
37
|
|
|
21
38
|
## [0.2.0] -- 2026-03-11
|
|
22
39
|
|
package/dispatcher-delivery.js
CHANGED
|
@@ -6,7 +6,7 @@ export function createDeliveryHelpers({ log, resolveDeliveryAlias }) {
|
|
|
6
6
|
return resolveDeliveryAlias(target);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
async function handleDelivery(job, content) {
|
|
9
|
+
async function handleDelivery(job, content, opts = {}) {
|
|
10
10
|
if (!['announce', 'announce-always'].includes(job.delivery_mode)) return;
|
|
11
11
|
if (!job.delivery_channel && !job.delivery_to) return;
|
|
12
12
|
|
|
@@ -24,7 +24,7 @@ export function createDeliveryHelpers({ log, resolveDeliveryAlias }) {
|
|
|
24
24
|
|
|
25
25
|
try {
|
|
26
26
|
const subject = (job.name || '').slice(0, 100);
|
|
27
|
-
|
|
27
|
+
const msg = {
|
|
28
28
|
from_agent: 'scheduler',
|
|
29
29
|
to_agent: 'main',
|
|
30
30
|
kind: 'result',
|
|
@@ -32,8 +32,15 @@ export function createDeliveryHelpers({ log, resolveDeliveryAlias }) {
|
|
|
32
32
|
body: content,
|
|
33
33
|
channel,
|
|
34
34
|
delivery_to: target,
|
|
35
|
-
}
|
|
36
|
-
|
|
35
|
+
};
|
|
36
|
+
// Pass image attachment paths so the message consumer can deliver them
|
|
37
|
+
// as photo/file attachments instead of plain text. Scripts signal this
|
|
38
|
+
// by writing [IMAGE:/path/to/file] markers to stdout.
|
|
39
|
+
if (opts.imageAttachments?.length > 0) {
|
|
40
|
+
msg.attachments = opts.imageAttachments;
|
|
41
|
+
}
|
|
42
|
+
sendMessage(msg);
|
|
43
|
+
log('info', `Enqueued: ${job.name}`, { channel, to: target, attachments: opts.imageAttachments?.length || 0 });
|
|
37
44
|
} catch (err) {
|
|
38
45
|
log('error', `Delivery enqueue failed: ${job.name}: ${err.message}`);
|
|
39
46
|
}
|
package/dispatcher-strategies.js
CHANGED
|
@@ -203,15 +203,19 @@ export async function finalizeDispatch(job, ctx, result, deps) {
|
|
|
203
203
|
const shouldAnnounce = ['announce', 'announce-always'].includes(job.delivery_mode)
|
|
204
204
|
&& deliveryContent?.trim();
|
|
205
205
|
|
|
206
|
+
const deliveryOpts = result.imageAttachments?.length > 0
|
|
207
|
+
? { imageAttachments: result.imageAttachments }
|
|
208
|
+
: {};
|
|
209
|
+
|
|
206
210
|
if (shouldAnnounce) {
|
|
207
211
|
if (result.deliveryOverride) {
|
|
208
|
-
await handleDelivery(job, result.deliveryOverride);
|
|
212
|
+
await handleDelivery(job, result.deliveryOverride, deliveryOpts);
|
|
209
213
|
} else if (result.status === 'error') {
|
|
210
214
|
const willRetry = (job.max_retries ?? 0) > 0 && (ctx.run.retry_count || 0) < job.max_retries;
|
|
211
215
|
const retryLabel = willRetry ? 'will retry' : 'no retries configured';
|
|
212
|
-
await handleDelivery(job, `\u26a0\ufe0f Job soft-failed (${retryLabel}): ${job.name}\n\n${deliveryContent}
|
|
216
|
+
await handleDelivery(job, `\u26a0\ufe0f Job soft-failed (${retryLabel}): ${job.name}\n\n${deliveryContent}`, deliveryOpts);
|
|
213
217
|
} else {
|
|
214
|
-
await handleDelivery(job, deliveryContent);
|
|
218
|
+
await handleDelivery(job, deliveryContent, deliveryOpts);
|
|
215
219
|
}
|
|
216
220
|
}
|
|
217
221
|
}
|
|
@@ -1008,6 +1012,9 @@ export async function executeShell(job, ctx, deps) {
|
|
|
1008
1012
|
result.summary = shellResult.summary;
|
|
1009
1013
|
result.errorMessage = shellResult.errorMessage;
|
|
1010
1014
|
result.content = shellResult.deliveryText;
|
|
1015
|
+
if (shellResult.imageAttachments?.length > 0) {
|
|
1016
|
+
result.imageAttachments = shellResult.imageAttachments;
|
|
1017
|
+
}
|
|
1011
1018
|
result.runFinishFields = {
|
|
1012
1019
|
context_summary: shellResult.contextSummary,
|
|
1013
1020
|
shell_exit_code: shellResult.exitCode,
|
package/jobs.js
CHANGED
|
@@ -305,17 +305,46 @@ export function validateJobSpec(opts, currentJob = null, mode = 'create') {
|
|
|
305
305
|
assertEnum('overlap_policy', merged.overlap_policy || 'skip', VALID_OVERLAP_POLICIES);
|
|
306
306
|
assertEnum('delivery_mode', merged.delivery_mode || 'announce', VALID_DELIVERY_MODES);
|
|
307
307
|
|
|
308
|
+
// INSERT-only: enforce delivery_to for all non-exempt jobs.
|
|
309
|
+
// Exempt from this requirement:
|
|
310
|
+
// - job_type='watchdog' (internal health monitor jobs)
|
|
311
|
+
// - name starts with 'watchdog:' (watchdog jobs by naming convention)
|
|
312
|
+
// - session_target='main' (injects into the main session, no chat routing needed)
|
|
313
|
+
// - delivery_mode='none' (explicitly opted out of delivery)
|
|
314
|
+
if (mode === 'create') {
|
|
315
|
+
const _target = merged.session_target || 'isolated';
|
|
316
|
+
const _dmode = merged.delivery_mode || 'announce';
|
|
317
|
+
const _type = merged.job_type || 'standard';
|
|
318
|
+
const _name = String(merged.name || '');
|
|
319
|
+
const _isExempt =
|
|
320
|
+
_type === 'watchdog' ||
|
|
321
|
+
_name.startsWith('watchdog:') ||
|
|
322
|
+
_target === 'main' ||
|
|
323
|
+
_dmode === 'none';
|
|
324
|
+
if (!_isExempt && (!merged.delivery_to || String(merged.delivery_to).trim() === '')) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
'delivery_to is required on job insert. Set it to the origin chat_id ' +
|
|
327
|
+
'(e.g. -5240776892 for AI Assisted Degeneracy, or 484946046 for Alex DM).'
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
308
332
|
// Enforce: delivery_to is required when delivery_mode is explicitly set to
|
|
309
333
|
// 'announce' or 'announce-always'. Validates on create (when delivery_mode is
|
|
310
334
|
// explicitly provided) and on update (when delivery_mode is being changed or
|
|
311
335
|
// the merged record would end up in announce mode without a delivery_to).
|
|
336
|
+
// Exempt: watchdog jobs, session_target='main' (no external chat routing needed).
|
|
312
337
|
{
|
|
313
338
|
const modeExplicitlySet = 'delivery_mode' in normalized;
|
|
314
339
|
const deliveryToExplicitlySet = 'delivery_to' in normalized;
|
|
315
340
|
const effectiveMode = merged.delivery_mode || 'announce';
|
|
316
341
|
const isAnnounceMode = ['announce', 'announce-always'].includes(effectiveMode);
|
|
342
|
+
const _announceExempt =
|
|
343
|
+
(merged.job_type || 'standard') === 'watchdog' ||
|
|
344
|
+
String(merged.name || '').startsWith('watchdog:') ||
|
|
345
|
+
(merged.session_target || 'isolated') === 'main';
|
|
317
346
|
|
|
318
|
-
if (isAnnounceMode && (mode === 'create' || modeExplicitlySet || deliveryToExplicitlySet)) {
|
|
347
|
+
if (isAnnounceMode && !_announceExempt && (mode === 'create' || modeExplicitlySet || deliveryToExplicitlySet)) {
|
|
319
348
|
// Re-evaluate: if mode is being set to announce OR delivery_to is being
|
|
320
349
|
// cleared on an announce-mode job, check the merged delivery_to is present.
|
|
321
350
|
if (!merged.delivery_to || (typeof merged.delivery_to === 'string' && merged.delivery_to.trim() === '')) {
|
package/package.json
CHANGED
package/scheduler-schema.js
CHANGED
|
@@ -22,7 +22,9 @@ export const SCHEDULER_SCHEMAS = {
|
|
|
22
22
|
max_trigger_fanout: { type: 'integer', min: 1, default: 25 },
|
|
23
23
|
delivery_mode: { type: 'string', enum: ['announce', 'announce-always', 'none'], default: 'announce' },
|
|
24
24
|
delivery_channel: { type: 'string', nullable: true },
|
|
25
|
-
|
|
25
|
+
// REQUIRED on insert for non-exempt jobs. Exempt: job_type='watchdog', name starts with 'watchdog:',
|
|
26
|
+
// session_target='main', or delivery_mode='none'. Set to the origin chat_id so results reach the right chat.
|
|
27
|
+
delivery_to: { type: 'string', nullable: true, required: 'non-system jobs', description: 'Target chat/user id for delivery (e.g. telegram chat_id). Required on insert for non-system jobs.' },
|
|
26
28
|
parent_id: { type: 'string', nullable: true },
|
|
27
29
|
trigger_on: { type: 'string', enum: ['success', 'failure', 'complete'], nullable: true },
|
|
28
30
|
trigger_delay_s: { type: 'integer', min: 0, default: 0 },
|
|
@@ -38,8 +40,8 @@ export const SCHEDULER_SCHEMAS = {
|
|
|
38
40
|
context_retrieval: { type: 'string', enum: ['none', 'recent', 'hybrid'], default: 'none' },
|
|
39
41
|
context_retrieval_limit: { type: 'integer', min: 1, default: 5 },
|
|
40
42
|
output_store_limit_bytes: { type: 'integer', min: 128, default: 65536 },
|
|
41
|
-
output_excerpt_limit_bytes: { type: 'integer', min: 64, default:
|
|
42
|
-
output_summary_limit_bytes: { type: 'integer', min: 64, default:
|
|
43
|
+
output_excerpt_limit_bytes: { type: 'integer', min: 64, default: 65536 },
|
|
44
|
+
output_summary_limit_bytes: { type: 'integer', min: 64, default: 65536 },
|
|
43
45
|
output_offload_threshold_bytes: { type: 'integer', min: 128, default: 65536 },
|
|
44
46
|
preferred_session_key: { type: 'string', nullable: true },
|
|
45
47
|
auth_profile: { type: 'string', nullable: true, description: 'Auth profile override: null=default, "inherit"=main session profile, or "provider:label"' },
|
package/schema.sql
CHANGED
|
@@ -81,8 +81,8 @@ CREATE TABLE IF NOT EXISTS jobs (
|
|
|
81
81
|
|
|
82
82
|
-- Output handling (v14)
|
|
83
83
|
output_store_limit_bytes INTEGER NOT NULL DEFAULT 65536,
|
|
84
|
-
output_excerpt_limit_bytes INTEGER NOT NULL DEFAULT
|
|
85
|
-
output_summary_limit_bytes INTEGER NOT NULL DEFAULT
|
|
84
|
+
output_excerpt_limit_bytes INTEGER NOT NULL DEFAULT 65536,
|
|
85
|
+
output_summary_limit_bytes INTEGER NOT NULL DEFAULT 65536,
|
|
86
86
|
output_offload_threshold_bytes INTEGER NOT NULL DEFAULT 65536,
|
|
87
87
|
|
|
88
88
|
-- Session continuity (v9)
|
|
@@ -118,7 +118,7 @@ function formatMessageForDelivery(msg, { brand = 'Scheduler' } = {}) {
|
|
|
118
118
|
const subject = msg.subject || 'Notification';
|
|
119
119
|
const header = `${brand} | ${subject} | ${age}`;
|
|
120
120
|
|
|
121
|
-
return `${header}\n\n${body}
|
|
121
|
+
return `${header}\n\n${body}`;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
package/shell-result.js
CHANGED
|
@@ -4,8 +4,8 @@ import { getResolvedDbPath } from './db.js';
|
|
|
4
4
|
import { ensureArtifactsDir, resolveArtifactsDir } from './paths.js';
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_STORE_LIMIT = 64 * 1024;
|
|
7
|
-
export const DEFAULT_EXCERPT_LIMIT =
|
|
8
|
-
export const DEFAULT_SUMMARY_LIMIT =
|
|
7
|
+
export const DEFAULT_EXCERPT_LIMIT = 64 * 1024;
|
|
8
|
+
export const DEFAULT_SUMMARY_LIMIT = 64 * 1024;
|
|
9
9
|
export const DEFAULT_OFFLOAD_THRESHOLD = 64 * 1024;
|
|
10
10
|
|
|
11
11
|
function toText(value) {
|
|
@@ -92,7 +92,24 @@ export function normalizeShellResult(
|
|
|
92
92
|
artifactsDir = resolveArtifactsDir({ dbPath: getResolvedDbPath() }),
|
|
93
93
|
} = {}
|
|
94
94
|
) {
|
|
95
|
-
const
|
|
95
|
+
const rawStdout = toText(stdout);
|
|
96
|
+
|
|
97
|
+
// Extract [IMAGE:path] markers from stdout before processing.
|
|
98
|
+
// Scripts can signal image attachments by writing lines like:
|
|
99
|
+
// [IMAGE:/tmp/chart.png]
|
|
100
|
+
// [IMAGE:/tmp/report.pdf]
|
|
101
|
+
// Extracted paths are passed through to the delivery layer.
|
|
102
|
+
const IMAGE_MARKER_RE = /^\[IMAGE:(\/[^\]\n]+)\]$/gm;
|
|
103
|
+
const imageAttachments = [];
|
|
104
|
+
let match;
|
|
105
|
+
while ((match = IMAGE_MARKER_RE.exec(rawStdout)) !== null) {
|
|
106
|
+
imageAttachments.push(match[1]);
|
|
107
|
+
}
|
|
108
|
+
// Strip markers from stdout so they don't appear in delivery text
|
|
109
|
+
const stdoutText = imageAttachments.length > 0
|
|
110
|
+
? rawStdout.replace(IMAGE_MARKER_RE, '').replace(/\n{3,}/g, '\n\n').trim()
|
|
111
|
+
: rawStdout;
|
|
112
|
+
|
|
96
113
|
const stderrText = toText(stderr);
|
|
97
114
|
const stdoutBytes = textBytes(stdoutText);
|
|
98
115
|
const stderrBytes = textBytes(stderrText);
|
|
@@ -144,6 +161,7 @@ export function normalizeShellResult(
|
|
|
144
161
|
stderrTruncated: stderrStored.truncated,
|
|
145
162
|
summary: truncateText(previewText, summaryLimit).text,
|
|
146
163
|
deliveryText: previewText,
|
|
164
|
+
imageAttachments,
|
|
147
165
|
errorMessage,
|
|
148
166
|
contextSummary: {
|
|
149
167
|
shell_result: {
|