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
package/schema.sql ADDED
@@ -0,0 +1,480 @@
1
+ -- OpenClaw Scheduler Schema (current: v1.7.0, schema version: 23)
2
+ -- Full standalone scheduler + message router
3
+
4
+ -- ============================================================
5
+ -- JOBS: scheduled tasks
6
+ -- ============================================================
7
+ CREATE TABLE IF NOT EXISTS jobs (
8
+ id TEXT PRIMARY KEY,
9
+ name TEXT NOT NULL,
10
+ enabled INTEGER NOT NULL DEFAULT 1,
11
+
12
+ -- Schedule: cron or one-shot 'at'
13
+ schedule_kind TEXT NOT NULL DEFAULT 'cron', -- 'cron' | 'at'
14
+ schedule_at TEXT DEFAULT NULL, -- SQLite UTC timestamp ('YYYY-MM-DD HH:MM:SS'), only for kind='at'
15
+ schedule_cron TEXT, -- NULL allowed for at-jobs (use sentinel '0 0 31 2 *' on old DBs)
16
+ schedule_tz TEXT NOT NULL DEFAULT 'UTC',
17
+
18
+ -- Execution
19
+ session_target TEXT NOT NULL DEFAULT 'isolated', -- 'main' | 'isolated' | 'shell'
20
+ agent_id TEXT DEFAULT 'main',
21
+
22
+ -- Payload
23
+ payload_kind TEXT NOT NULL, -- 'systemEvent' | 'agentTurn' | 'shellCommand'
24
+ payload_message TEXT NOT NULL,
25
+ payload_model TEXT,
26
+ payload_thinking TEXT,
27
+ payload_timeout_seconds INTEGER DEFAULT 120,
28
+ execution_intent TEXT NOT NULL DEFAULT 'execute', -- 'execute' | 'plan'
29
+ execution_read_only INTEGER NOT NULL DEFAULT 0,
30
+
31
+ -- Overlap & timeout
32
+ overlap_policy TEXT NOT NULL DEFAULT 'skip', -- 'skip' | 'allow' | 'queue'
33
+ run_timeout_ms INTEGER NOT NULL DEFAULT 300000,
34
+ max_queued_dispatches INTEGER NOT NULL DEFAULT 25,
35
+ max_pending_approvals INTEGER NOT NULL DEFAULT 10,
36
+ max_trigger_fanout INTEGER NOT NULL DEFAULT 25,
37
+
38
+ -- Delivery
39
+ delivery_mode TEXT DEFAULT 'announce', -- 'announce' | 'announce-always' | 'none'
40
+ delivery_channel TEXT,
41
+ delivery_to TEXT,
42
+
43
+ -- Metadata
44
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
45
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
46
+ delete_after_run INTEGER NOT NULL DEFAULT 0,
47
+ ttl_hours INTEGER DEFAULT NULL, -- auto-delete N hours after last_run_at if terminal status
48
+
49
+ -- Workflow chaining (v3)
50
+ parent_id TEXT, -- soft ref to parent job id
51
+ trigger_on TEXT, -- 'success' | 'failure' | 'complete' | NULL
52
+ trigger_delay_s INTEGER DEFAULT 0,
53
+
54
+ -- Output-based trigger condition (v4)
55
+ trigger_condition TEXT DEFAULT NULL, -- 'contains:ALERT' | 'regex:pattern' | NULL
56
+
57
+ -- Retry logic (v3b)
58
+ max_retries INTEGER DEFAULT 0, -- 0 = no retry
59
+
60
+ -- Queue overlap (v3c)
61
+ queued_count INTEGER DEFAULT 0, -- pending dispatches waiting for current run
62
+
63
+ -- Sub-agent scope (v3c)
64
+ payload_scope TEXT NOT NULL DEFAULT 'own', -- 'own' | 'global'
65
+
66
+ -- Resource pool (concurrency across different jobs)
67
+ resource_pool TEXT DEFAULT NULL,
68
+
69
+ -- Delivery semantics (v5)
70
+ delivery_guarantee TEXT DEFAULT 'at-most-once', -- 'at-most-once'|'at-least-once'
71
+ job_class TEXT DEFAULT 'standard', -- 'standard'|'pre_compaction_flush'
72
+
73
+ -- HITL approval gates (v5)
74
+ approval_required INTEGER DEFAULT 0,
75
+ approval_timeout_s INTEGER DEFAULT 3600,
76
+ approval_auto TEXT DEFAULT 'reject', -- 'approve'|'reject'
77
+
78
+ -- Context retrieval (v5)
79
+ context_retrieval TEXT DEFAULT 'none', -- 'none'|'recent'|'hybrid'
80
+ context_retrieval_limit INTEGER DEFAULT 5,
81
+
82
+ -- Output handling (v14)
83
+ output_store_limit_bytes INTEGER NOT NULL DEFAULT 65536,
84
+ output_excerpt_limit_bytes INTEGER NOT NULL DEFAULT 2000,
85
+ output_summary_limit_bytes INTEGER NOT NULL DEFAULT 5000,
86
+ output_offload_threshold_bytes INTEGER NOT NULL DEFAULT 65536,
87
+
88
+ -- Session continuity (v9)
89
+ preferred_session_key TEXT DEFAULT NULL, -- pass to gateway for session reuse
90
+
91
+ -- Auth profile override (v16)
92
+ auth_profile TEXT DEFAULT NULL, -- null=default, 'inherit'=main session profile, or 'provider:label'
93
+
94
+ -- Delivery opt-out (v19)
95
+ delivery_opt_out_reason TEXT DEFAULT NULL, -- set when delivery_mode='none' to explicitly skip delivery
96
+
97
+ -- Origin tracking (v20)
98
+ origin TEXT DEFAULT NULL, -- where job was dispatched from: "telegram:<chat_id>", "system", etc.
99
+
100
+ -- v0.2 Identity (v22)
101
+ identity_principal TEXT DEFAULT NULL,
102
+ identity_run_as TEXT DEFAULT NULL,
103
+ identity_attestation TEXT DEFAULT NULL,
104
+ identity_ref TEXT DEFAULT NULL,
105
+ identity_subject_kind TEXT DEFAULT NULL,
106
+ identity_subject_principal TEXT DEFAULT NULL,
107
+ identity_trust_level TEXT DEFAULT NULL,
108
+ identity_delegation_mode TEXT DEFAULT NULL,
109
+ identity TEXT DEFAULT NULL,
110
+
111
+ -- v0.2 Authorization Proof (v22)
112
+ authorization_proof_ref TEXT DEFAULT NULL,
113
+ authorization_proof TEXT DEFAULT NULL,
114
+
115
+ -- v0.2 Authorization (v22)
116
+ authorization_ref TEXT DEFAULT NULL,
117
+ authorization TEXT DEFAULT NULL,
118
+
119
+ -- v0.2 Evidence (v22)
120
+ evidence_ref TEXT DEFAULT NULL,
121
+ evidence TEXT DEFAULT NULL,
122
+
123
+ -- v0.2 Contract (v22)
124
+ contract_required_trust_level TEXT DEFAULT NULL,
125
+ contract_trust_enforcement TEXT DEFAULT NULL,
126
+ contract_sandbox TEXT DEFAULT NULL,
127
+ contract_allowed_paths TEXT DEFAULT NULL,
128
+ contract_network TEXT DEFAULT NULL,
129
+ contract_max_cost_usd REAL DEFAULT NULL,
130
+ contract_audit TEXT DEFAULT NULL,
131
+
132
+ -- v0.2 Child Credential Policy (v23)
133
+ child_credential_policy TEXT DEFAULT NULL,
134
+
135
+ -- Watchdog monitoring (v13)
136
+ job_type TEXT NOT NULL DEFAULT 'standard', -- 'standard' | 'watchdog'
137
+ watchdog_target_label TEXT, -- label of the task being monitored
138
+ watchdog_check_cmd TEXT, -- shell command to check target status
139
+ watchdog_timeout_min INTEGER, -- alert if target running longer than this
140
+ watchdog_alert_channel TEXT, -- e.g. 'telegram'
141
+ watchdog_alert_target TEXT, -- e.g. '<telegram-user-id>'
142
+ watchdog_self_destruct INTEGER NOT NULL DEFAULT 1, -- delete when target done
143
+ watchdog_started_at TEXT, -- ISO timestamp when target was dispatched
144
+
145
+ -- Scheduling state (denormalized)
146
+ next_run_at TEXT,
147
+ last_run_at TEXT,
148
+ last_status TEXT,
149
+ consecutive_errors INTEGER NOT NULL DEFAULT 0,
150
+
151
+ -- Delivery target constraint: announce modes require a delivery_to
152
+ CHECK (
153
+ delivery_mode NOT IN ('announce', 'announce-always')
154
+ OR (delivery_to IS NOT NULL AND delivery_to != '')
155
+ )
156
+ );
157
+
158
+ CREATE INDEX IF NOT EXISTS idx_jobs_next_run ON jobs(next_run_at) WHERE enabled = 1;
159
+ CREATE INDEX IF NOT EXISTS idx_jobs_parent ON jobs(parent_id) WHERE parent_id IS NOT NULL;
160
+
161
+ -- ============================================================
162
+ -- RUNS: job execution history with heartbeat tracking
163
+ -- ============================================================
164
+ CREATE TABLE IF NOT EXISTS runs (
165
+ id TEXT PRIMARY KEY,
166
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
167
+ status TEXT NOT NULL DEFAULT 'pending', -- pending|running|ok|error|timeout|skipped|awaiting_approval|approved|cancelled|crashed
168
+
169
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
170
+ finished_at TEXT,
171
+ duration_ms INTEGER,
172
+
173
+ -- Implicit heartbeat (updated by dispatcher checking session activity)
174
+ last_heartbeat TEXT NOT NULL DEFAULT (datetime('now')),
175
+
176
+ -- Session tracking
177
+ session_key TEXT,
178
+ session_id TEXT,
179
+
180
+ -- Result
181
+ summary TEXT,
182
+ error_message TEXT,
183
+ shell_exit_code INTEGER,
184
+ shell_signal TEXT,
185
+ shell_timed_out INTEGER NOT NULL DEFAULT 0,
186
+ shell_stdout TEXT,
187
+ shell_stderr TEXT,
188
+ shell_stdout_path TEXT,
189
+ shell_stderr_path TEXT,
190
+ shell_stdout_bytes INTEGER NOT NULL DEFAULT 0,
191
+ shell_stderr_bytes INTEGER NOT NULL DEFAULT 0,
192
+ dispatched_at TEXT,
193
+ run_timeout_ms INTEGER NOT NULL DEFAULT 300000,
194
+
195
+ -- Retry tracking (v3b)
196
+ retry_count INTEGER DEFAULT 0,
197
+ retry_of TEXT, -- original run id if this is a retry
198
+ triggered_by_run TEXT, -- parent run id if this run was chain-triggered
199
+ dispatch_queue_id TEXT REFERENCES job_dispatch_queue(id) ON DELETE SET NULL,
200
+
201
+ -- Context & replay (v5)
202
+ context_summary TEXT, -- JSON: {messages_injected,scope,...}
203
+ replay_of TEXT, -- run id if this is a crash replay
204
+
205
+ -- Idempotency (v7)
206
+ idempotency_key TEXT, -- deterministic key for dedup
207
+
208
+ -- v0.2 Outcomes (v22)
209
+ identity_resolved TEXT DEFAULT NULL,
210
+ trust_evaluation TEXT DEFAULT NULL,
211
+ authorization_decision TEXT DEFAULT NULL,
212
+ authorization_proof_verification TEXT DEFAULT NULL,
213
+ evidence_record TEXT DEFAULT NULL,
214
+ credential_handoff_summary TEXT DEFAULT NULL
215
+ );
216
+
217
+ CREATE INDEX IF NOT EXISTS idx_runs_job_id ON runs(job_id);
218
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status) WHERE status = 'running';
219
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_idempotency ON runs(idempotency_key) WHERE idempotency_key IS NOT NULL;
220
+ CREATE INDEX IF NOT EXISTS idx_runs_dispatch_queue ON runs(dispatch_queue_id) WHERE dispatch_queue_id IS NOT NULL;
221
+
222
+ -- ============================================================
223
+ -- MESSAGES: inter-agent message queue
224
+ -- ============================================================
225
+ CREATE TABLE IF NOT EXISTS messages (
226
+ id TEXT PRIMARY KEY,
227
+
228
+ -- Routing
229
+ from_agent TEXT NOT NULL, -- sender agent id or 'scheduler' or 'user'
230
+ to_agent TEXT NOT NULL, -- recipient agent id or 'broadcast'
231
+ team_id TEXT, -- optional team routing namespace
232
+ member_id TEXT, -- optional team member routing key
233
+ task_id TEXT, -- optional team task correlation key
234
+ reply_to TEXT REFERENCES messages(id) ON DELETE SET NULL, -- threading
235
+
236
+ -- Content
237
+ kind TEXT NOT NULL DEFAULT 'text', -- 'text' | 'task' | 'result' | 'status' | 'system'
238
+ subject TEXT, -- optional subject line
239
+ body TEXT NOT NULL,
240
+ metadata TEXT, -- JSON blob for structured data
241
+
242
+ -- Priority & delivery
243
+ priority INTEGER NOT NULL DEFAULT 0, -- higher = more urgent (0=normal, 1=high, 2=urgent)
244
+ channel TEXT, -- optional: route via specific channel
245
+ delivery_to TEXT, -- optional: target chat/user id for outbound delivery
246
+
247
+ -- Status
248
+ status TEXT NOT NULL DEFAULT 'pending', -- pending|delivered|read|expired|failed
249
+ delivered_at TEXT,
250
+ read_at TEXT,
251
+ ack_required INTEGER NOT NULL DEFAULT 0, -- message requires explicit ACK
252
+ ack_at TEXT, -- explicit acknowledgement timestamp
253
+ delivery_attempts INTEGER NOT NULL DEFAULT 0, -- outbound delivery attempts
254
+ last_error TEXT, -- last delivery/adapter error
255
+ team_mapped_at TEXT, -- when team adapter projected this message
256
+ expires_at TEXT, -- optional TTL
257
+
258
+ -- Metadata
259
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
260
+
261
+ -- Link to job/run if this message is job-related
262
+ job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
263
+ run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
264
+
265
+ -- Typed message owner (v5)
266
+ owner TEXT -- originator of typed message
267
+ );
268
+
269
+ CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent, status);
270
+ CREATE INDEX IF NOT EXISTS idx_messages_from ON messages(from_agent);
271
+ CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at);
272
+ CREATE INDEX IF NOT EXISTS idx_messages_pending ON messages(to_agent, status, priority DESC) WHERE status = 'pending';
273
+ CREATE INDEX IF NOT EXISTS idx_messages_team ON messages(team_id, member_id, status) WHERE team_id IS NOT NULL;
274
+ CREATE INDEX IF NOT EXISTS idx_messages_task ON messages(team_id, task_id, created_at) WHERE team_id IS NOT NULL AND task_id IS NOT NULL;
275
+ CREATE INDEX IF NOT EXISTS idx_messages_ack_pending ON messages(ack_required, ack_at, status) WHERE ack_required = 1 AND ack_at IS NULL;
276
+
277
+ -- ============================================================
278
+ -- AGENTS: registered agents and status
279
+ -- ============================================================
280
+ CREATE TABLE IF NOT EXISTS agents (
281
+ id TEXT PRIMARY KEY, -- agent id (e.g. 'main', 'ops')
282
+ name TEXT,
283
+ status TEXT NOT NULL DEFAULT 'idle', -- idle|busy|offline
284
+ last_seen_at TEXT,
285
+ session_key TEXT, -- current active session key
286
+ capabilities TEXT, -- JSON array of capability tags
287
+ delivery_channel TEXT, -- e.g. 'telegram'
288
+ delivery_to TEXT, -- e.g. '<telegram-user-id>'
289
+ brand_name TEXT, -- display name for notifications
290
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
291
+ );
292
+
293
+ -- ============================================================
294
+ -- DELIVERY ALIASES: named targets for job delivery
295
+ -- ============================================================
296
+ CREATE TABLE IF NOT EXISTS delivery_aliases (
297
+ alias TEXT PRIMARY KEY,
298
+ channel TEXT NOT NULL,
299
+ target TEXT NOT NULL,
300
+ description TEXT,
301
+ created_at TEXT DEFAULT (datetime('now'))
302
+ );
303
+
304
+ -- Example delivery aliases -- replace targets with real Telegram chat/user IDs.
305
+ -- These placeholder IDs are non-functional; run `openclaw-scheduler aliases update`
306
+ -- or INSERT your own rows to configure delivery routing.
307
+ -- Example delivery aliases (not seeded — add via CLI or SQL):
308
+ -- INSERT INTO delivery_aliases (alias, channel, target, description) VALUES
309
+ -- ('team_room', 'telegram', '<your-chat-id>', 'Team room'),
310
+ -- ('owner_dm', 'telegram', '<your-user-id>', 'Owner DM');
311
+
312
+ -- ============================================================
313
+ -- APPROVALS: HITL approval gates (v5)
314
+ -- ============================================================
315
+ CREATE TABLE IF NOT EXISTS approvals (
316
+ id TEXT PRIMARY KEY,
317
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
318
+ run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
319
+ dispatch_queue_id TEXT REFERENCES job_dispatch_queue(id) ON DELETE SET NULL,
320
+ status TEXT NOT NULL DEFAULT 'pending', -- pending|approved|rejected|timed_out|dispatched
321
+ requested_at TEXT NOT NULL DEFAULT (datetime('now')),
322
+ resolved_at TEXT,
323
+ resolved_by TEXT, -- 'operator'|'timeout'|'api'
324
+ notes TEXT
325
+ );
326
+
327
+ CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status) WHERE status = 'pending';
328
+ CREATE INDEX IF NOT EXISTS idx_approvals_job ON approvals(job_id);
329
+ CREATE INDEX IF NOT EXISTS idx_approvals_dispatch_queue ON approvals(dispatch_queue_id) WHERE dispatch_queue_id IS NOT NULL;
330
+
331
+ -- ============================================================
332
+ -- DISPATCH QUEUE: durable non-cron invocations (v11)
333
+ -- ============================================================
334
+ CREATE TABLE IF NOT EXISTS job_dispatch_queue (
335
+ id TEXT PRIMARY KEY,
336
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
337
+ dispatch_kind TEXT NOT NULL, -- manual|chain|retry
338
+ status TEXT NOT NULL DEFAULT 'pending', -- pending|claimed|awaiting_approval|done|cancelled
339
+ scheduled_for TEXT NOT NULL,
340
+ source_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
341
+ retry_of_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
342
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
343
+ claimed_at TEXT,
344
+ processed_at TEXT
345
+ );
346
+ CREATE INDEX IF NOT EXISTS idx_dispatch_queue_due ON job_dispatch_queue(status, scheduled_for);
347
+ CREATE INDEX IF NOT EXISTS idx_dispatch_queue_job ON job_dispatch_queue(job_id, created_at DESC);
348
+ CREATE INDEX IF NOT EXISTS idx_dispatch_queue_source_run ON job_dispatch_queue(source_run_id) WHERE source_run_id IS NOT NULL;
349
+
350
+ -- ============================================================
351
+ -- IDEMPOTENCY LEDGER: tracks claimed idempotency keys (v7)
352
+ -- ============================================================
353
+ CREATE TABLE IF NOT EXISTS idempotency_ledger (
354
+ key TEXT PRIMARY KEY,
355
+ job_id TEXT NOT NULL,
356
+ run_id TEXT NOT NULL,
357
+ status TEXT NOT NULL DEFAULT 'claimed', -- claimed | released
358
+ claimed_at TEXT NOT NULL DEFAULT (datetime('now')),
359
+ released_at TEXT,
360
+ result_hash TEXT, -- optional: hash of the result for verification
361
+ expires_at TEXT NOT NULL -- auto-expire old entries to prevent unbounded growth
362
+ );
363
+ CREATE INDEX IF NOT EXISTS idx_idem_expires ON idempotency_ledger(expires_at);
364
+ CREATE INDEX IF NOT EXISTS idx_idem_job ON idempotency_ledger(job_id);
365
+
366
+ -- ============================================================
367
+ -- TASK TRACKER: dead-man's-switch monitoring for sub-agent teams (v6)
368
+ -- ============================================================
369
+ CREATE TABLE IF NOT EXISTS task_tracker (
370
+ id TEXT PRIMARY KEY, -- unique task group id
371
+ name TEXT NOT NULL, -- human label e.g. "v5-agent-team"
372
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
373
+ created_by TEXT NOT NULL DEFAULT 'main', -- who spawned the task group
374
+ expected_agents TEXT NOT NULL, -- JSON array: ["schema-and-data","runtime-integration","rfc-docs"]
375
+ timeout_s INTEGER NOT NULL DEFAULT 600,
376
+ status TEXT NOT NULL DEFAULT 'active', -- active|completed|failed|timed_out
377
+ completed_at TEXT,
378
+ delivery_channel TEXT, -- where to send updates
379
+ delivery_to TEXT, -- target for updates
380
+ summary TEXT -- final summary on completion
381
+ );
382
+ CREATE INDEX IF NOT EXISTS idx_task_tracker_status ON task_tracker(status) WHERE status = 'active';
383
+
384
+ CREATE TABLE IF NOT EXISTS task_tracker_agents (
385
+ id TEXT PRIMARY KEY,
386
+ tracker_id TEXT NOT NULL REFERENCES task_tracker(id) ON DELETE CASCADE,
387
+ agent_label TEXT NOT NULL, -- matches label in expected_agents
388
+ status TEXT NOT NULL DEFAULT 'pending', -- pending|running|completed|failed|dead
389
+ started_at TEXT,
390
+ finished_at TEXT,
391
+ exit_message TEXT, -- agent's final status message
392
+ error TEXT,
393
+ session_key TEXT, -- OpenClaw session key for auto-correlation (v8)
394
+ last_heartbeat TEXT -- last activity detected (CLI or auto-correlation)
395
+ );
396
+ CREATE INDEX IF NOT EXISTS idx_tta_tracker ON task_tracker_agents(tracker_id);
397
+ CREATE INDEX IF NOT EXISTS idx_tta_status ON task_tracker_agents(status) WHERE status IN ('pending','running');
398
+ CREATE INDEX IF NOT EXISTS idx_tta_session_key ON task_tracker_agents(session_key) WHERE session_key IS NOT NULL;
399
+
400
+ -- ============================================================
401
+ -- MESSAGE RECEIPTS: explicit delivery/ack audit trail (v10)
402
+ -- ============================================================
403
+ CREATE TABLE IF NOT EXISTS message_receipts (
404
+ id TEXT PRIMARY KEY,
405
+ message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
406
+ event_type TEXT NOT NULL, -- attempt|error|ack|read|adapter
407
+ attempt INTEGER,
408
+ actor TEXT, -- dispatcher|consumer|agent|team-adapter|operator
409
+ detail TEXT,
410
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
411
+ );
412
+ CREATE INDEX IF NOT EXISTS idx_receipts_message ON message_receipts(message_id, created_at DESC);
413
+
414
+ -- ============================================================
415
+ -- TEAM ADAPTER TABLES: mailbox/task projection + gates (v10)
416
+ -- ============================================================
417
+ CREATE TABLE IF NOT EXISTS team_tasks (
418
+ team_id TEXT NOT NULL,
419
+ id TEXT NOT NULL, -- task id within a team namespace
420
+ member_id TEXT, -- owner/assignee
421
+ source_message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
422
+ title TEXT,
423
+ status TEXT NOT NULL DEFAULT 'open', -- open|blocked|completed|failed
424
+ gate_tracker_id TEXT REFERENCES task_tracker(id) ON DELETE SET NULL,
425
+ gate_status TEXT, -- waiting|passed|failed|NULL
426
+ last_error TEXT,
427
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
428
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
429
+ completed_at TEXT,
430
+ PRIMARY KEY (team_id, id)
431
+ );
432
+ CREATE INDEX IF NOT EXISTS idx_team_tasks_status ON team_tasks(team_id, status, updated_at DESC);
433
+ CREATE INDEX IF NOT EXISTS idx_team_tasks_gate ON team_tasks(gate_tracker_id) WHERE gate_tracker_id IS NOT NULL;
434
+
435
+ CREATE TABLE IF NOT EXISTS team_mailbox_events (
436
+ id TEXT PRIMARY KEY,
437
+ team_id TEXT NOT NULL,
438
+ member_id TEXT,
439
+ task_id TEXT,
440
+ message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
441
+ event_type TEXT NOT NULL, -- mailbox|task_created|task_message|gate_open|gate_passed|gate_failed|ack
442
+ payload TEXT, -- JSON details
443
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
444
+ );
445
+ CREATE INDEX IF NOT EXISTS idx_team_events_team ON team_mailbox_events(team_id, created_at DESC);
446
+ CREATE INDEX IF NOT EXISTS idx_team_events_task ON team_mailbox_events(team_id, task_id, created_at DESC) WHERE task_id IS NOT NULL;
447
+
448
+ -- ============================================================
449
+ -- MIGRATION LOG
450
+ -- ============================================================
451
+ CREATE TABLE IF NOT EXISTS schema_migrations (
452
+ version INTEGER PRIMARY KEY,
453
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
454
+ );
455
+
456
+ -- Fresh installs seed all versions 1-23 (all columns already in schema above).
457
+ -- Existing installs are brought up to v23 by migrate-consolidate.js.
458
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (1);
459
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (2);
460
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (3);
461
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (4);
462
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (5);
463
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (6);
464
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (7);
465
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (8);
466
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (9);
467
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (10);
468
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (11);
469
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (12);
470
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (13);
471
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (14);
472
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (15);
473
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (16);
474
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (17);
475
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (18);
476
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (19);
477
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (20);
478
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (21);
479
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (22);
480
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (23);
@@ -0,0 +1,65 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { execFileSync } from 'child_process';
4
+
5
+ /**
6
+ * Check if a binary is available in PATH.
7
+ */
8
+ function commandExists(cmd) {
9
+ try {
10
+ const isWin = process.platform === 'win32';
11
+ execFileSync(isWin ? 'where' : 'which', [cmd], { stdio: 'pipe' });
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Resolve the dispatch CLI path with backward-compatible fallbacks.
20
+ * Priority:
21
+ * 0) DISPATCH_CLI env override -- explicit override always wins
22
+ * 1) openclaw-scheduler bin (in PATH) -- preferred public interface for npm consumers
23
+ * 2) $OPENCLAW_HOME/scheduler/dispatch/index.mjs
24
+ * 3) $OPENCLAW_HOME/dispatch/index.mjs
25
+ *
26
+ * @param {object} env - Environment variables (defaults to process.env).
27
+ * @param {function} exists - File existence check (defaults to existsSync).
28
+ * @param {function} cmdExists - Binary-in-PATH check (defaults to commandExists).
29
+ * @returns {string} Absolute file path to the dispatch CLI entry point,
30
+ * or the bare binary name 'openclaw-scheduler' when found in PATH.
31
+ */
32
+ export function resolveDispatchCliPath(env = process.env, exists = existsSync, cmdExists = commandExists) {
33
+ const homeDir = env.HOME || '';
34
+ const openclawHome = env.OPENCLAW_HOME
35
+ || (homeDir ? join(homeDir, '.openclaw') : '.openclaw');
36
+
37
+ // Explicit env override always wins
38
+ if (env.DISPATCH_CLI && exists(env.DISPATCH_CLI)) return env.DISPATCH_CLI;
39
+
40
+ // Prefer installed bin in PATH (canonical entry point for npm consumers)
41
+ if (cmdExists('openclaw-scheduler')) return 'openclaw-scheduler';
42
+
43
+ // Fall back to well-known file paths for dev/manual installs
44
+ const candidates = [
45
+ join(openclawHome, 'scheduler', 'dispatch', 'index.mjs'),
46
+ join(openclawHome, 'dispatch', 'index.mjs'),
47
+ ];
48
+
49
+ return candidates.find(p => exists(p)) || candidates[0] || 'dispatch/index.mjs';
50
+ }
51
+
52
+ /**
53
+ * Resolve a scheduler job name to a dispatch label in labels.json.
54
+ * Supports current and legacy watcher prefixes.
55
+ */
56
+ export function resolveDispatchLabel(jobName, labels = {}) {
57
+ if (labels[jobName]) return jobName;
58
+ // Match any branded deliver job: <brand>-deliver:<label>
59
+ const deliverMatch = jobName.match(/^.+-deliver:(.+)$/);
60
+ if (deliverMatch) {
61
+ const suffix = deliverMatch[1];
62
+ if (labels[suffix]) return suffix;
63
+ }
64
+ return null;
65
+ }