openclaw-scheduler 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +302 -0
- package/BEST-PRACTICES.md +506 -0
- package/CHANGELOG.md +82 -0
- package/CODE_OF_CONDUCT.md +22 -0
- package/CONTEXT.md +26 -0
- package/CONTRIBUTING.md +73 -0
- package/IMPLEMENTATION_SPEC.md +170 -0
- package/INSTALL-ADDITIONAL-HOST.md +333 -0
- package/INSTALL-LINUX.md +419 -0
- package/INSTALL-WINDOWS.md +305 -0
- package/INSTALL.md +364 -0
- package/JOB-QUICK-REF.md +222 -0
- package/LICENSE +21 -0
- package/QUICK-START.md +256 -0
- package/README.md +2170 -0
- package/SECURITY.md +34 -0
- package/UNINSTALL.md +129 -0
- package/UPGRADING.md +436 -0
- package/agents.js +67 -0
- package/approval.js +107 -0
- package/backup.js +390 -0
- package/bin/openclaw-scheduler.js +138 -0
- package/cli.js +1083 -0
- package/db.js +122 -0
- package/dispatch/529-recovery.mjs +204 -0
- package/dispatch/README.md +372 -0
- package/dispatch/config.example.json +24 -0
- package/dispatch/deliver-watcher.sh +57 -0
- package/dispatch/hooks.mjs +171 -0
- package/dispatch/index.mjs +1836 -0
- package/dispatch/watcher.mjs +1396 -0
- package/dispatch-queue.js +112 -0
- package/dispatcher-approvals.js +96 -0
- package/dispatcher-delivery.js +43 -0
- package/dispatcher-maintenance.js +242 -0
- package/dispatcher-shell.js +29 -0
- package/dispatcher-strategies.js +1280 -0
- package/dispatcher-utils.js +81 -0
- package/dispatcher.js +855 -0
- package/docs/adr-schedule-ownership.md +73 -0
- package/docs/gateway-contract.md +904 -0
- package/docs/plans/2026-03-09-fix-typescript-types.md +91 -0
- package/docs/plans/2026-03-09-test-coverage-gaps.md +83 -0
- package/docs/plans/2026-03-10-dispatcher-refactor.md +801 -0
- package/docs/trust-architecture.md +266 -0
- package/gateway.js +473 -0
- package/idempotency.js +119 -0
- package/index.d.ts +864 -0
- package/index.js +17 -0
- package/jobs.js +1224 -0
- package/messages.js +357 -0
- package/migrate-consolidate.js +694 -0
- package/migrate.js +125 -0
- package/package.json +130 -0
- package/paths.js +79 -0
- package/prompt-context.js +94 -0
- package/retrieval.js +176 -0
- package/runs.js +270 -0
- package/scheduler-schema.js +101 -0
- package/schema.sql +480 -0
- package/scripts/dispatch-cli-utils.mjs +65 -0
- package/scripts/inbox-consumer.mjs +288 -0
- package/scripts/stuck-detector.sh +18 -0
- package/scripts/stuck-run-detector.mjs +333 -0
- package/scripts/telegram-webhook-check.mjs +238 -0
- package/setup.mjs +724 -0
- package/shell-result.js +214 -0
- package/task-tracker.js +300 -0
- package/team-adapter.js +335 -0
- package/v02-runtime.js +599 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
Contributions should improve one of these areas:
|
|
6
|
+
|
|
7
|
+
- runtime reliability
|
|
8
|
+
- workflow and queue semantics
|
|
9
|
+
- installation and service management
|
|
10
|
+
- package/install ergonomics
|
|
11
|
+
- documentation and tests
|
|
12
|
+
|
|
13
|
+
## Ground Rules
|
|
14
|
+
|
|
15
|
+
- preserve durable runtime behavior
|
|
16
|
+
- do not remove backward-compatible CLI or schema behavior casually
|
|
17
|
+
- update docs when installation or runtime behavior changes
|
|
18
|
+
- update tests when changing scheduler semantics, payload validation, or delivery behavior
|
|
19
|
+
|
|
20
|
+
## Development
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install
|
|
24
|
+
npm test
|
|
25
|
+
npm run lint
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Local Verification Gate
|
|
29
|
+
|
|
30
|
+
Before pushing or opening a PR, run the full local gate:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run verify:local
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This runs, in order:
|
|
37
|
+
|
|
38
|
+
1. Lint (`eslint`)
|
|
39
|
+
2. TypeScript declaration smoke tests
|
|
40
|
+
3. Full test suite (in-memory SQLite) -- must end with **0 failed**
|
|
41
|
+
4. Coverage floor checks (statement, branch, function, line)
|
|
42
|
+
|
|
43
|
+
The same gate runs automatically via `prepublishOnly` before any `npm publish`.
|
|
44
|
+
|
|
45
|
+
If you add new features or fix bugs, add tests. The test count should only go up. Coverage expectations are enforced by the verify script -- if you drop below the floor, the gate fails.
|
|
46
|
+
|
|
47
|
+
## Branch Model
|
|
48
|
+
|
|
49
|
+
All PRs target `main`. There are no long-lived feature branches.
|
|
50
|
+
|
|
51
|
+
## Release Process
|
|
52
|
+
|
|
53
|
+
1. `npm run verify:local` -- must pass completely
|
|
54
|
+
2. `npm version <patch|minor|major>`
|
|
55
|
+
3. `npm publish` -- `prepublishOnly` re-runs the verification gate
|
|
56
|
+
4. Push the version commit and tag: `git push && git push --tags`
|
|
57
|
+
|
|
58
|
+
### Agent-Facing Documentation
|
|
59
|
+
|
|
60
|
+
The following files ship in the npm package for agent adoption:
|
|
61
|
+
|
|
62
|
+
- `AGENTS.md` -- discovery flow, working rules, CLI commands
|
|
63
|
+
- `CONTEXT.md` -- repo positioning, design bias
|
|
64
|
+
- `JOB-QUICK-REF.md` -- copy-paste job patterns, field reference
|
|
65
|
+
- `docs/` -- gateway contract, trust architecture, ADRs
|
|
66
|
+
|
|
67
|
+
Update these when adding new features or changing the CLI API.
|
|
68
|
+
|
|
69
|
+
## Pull Requests
|
|
70
|
+
|
|
71
|
+
- explain whether the change affects runtime behavior, package/install behavior, or both
|
|
72
|
+
- call out migration or compatibility risk explicitly
|
|
73
|
+
- include verification steps
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Implementation Spec — Scheduler v5 Features
|
|
2
|
+
|
|
3
|
+
## New Schema (consolidated into schema baseline)
|
|
4
|
+
|
|
5
|
+
### Jobs table — new columns:
|
|
6
|
+
```sql
|
|
7
|
+
ALTER TABLE jobs ADD COLUMN delivery_guarantee TEXT DEFAULT 'at-most-once'; -- 'at-most-once'|'at-least-once'
|
|
8
|
+
ALTER TABLE jobs ADD COLUMN job_class TEXT DEFAULT 'standard'; -- 'standard'|'pre_compaction_flush'
|
|
9
|
+
ALTER TABLE jobs ADD COLUMN approval_required INTEGER DEFAULT 0; -- HITL gate
|
|
10
|
+
ALTER TABLE jobs ADD COLUMN approval_timeout_s INTEGER DEFAULT 3600;
|
|
11
|
+
ALTER TABLE jobs ADD COLUMN approval_auto TEXT DEFAULT 'reject'; -- 'approve'|'reject'
|
|
12
|
+
ALTER TABLE jobs ADD COLUMN context_retrieval TEXT DEFAULT 'none'; -- 'none'|'recent'|'hybrid'
|
|
13
|
+
ALTER TABLE jobs ADD COLUMN context_retrieval_limit INTEGER DEFAULT 5;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Runs table — new columns:
|
|
17
|
+
```sql
|
|
18
|
+
ALTER TABLE runs ADD COLUMN context_summary TEXT; -- JSON: {messages_injected,scope,aliases_resolved,...}
|
|
19
|
+
ALTER TABLE runs ADD COLUMN replay_of TEXT; -- run id if this is a crash replay
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Messages table — new column:
|
|
23
|
+
```sql
|
|
24
|
+
ALTER TABLE messages ADD COLUMN owner TEXT; -- originator of typed message
|
|
25
|
+
```
|
|
26
|
+
(kind enum extends to include: 'decision','constraint','fact','preference' alongside existing)
|
|
27
|
+
|
|
28
|
+
### New table: approvals
|
|
29
|
+
```sql
|
|
30
|
+
CREATE TABLE IF NOT EXISTS approvals (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
33
|
+
run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
34
|
+
status TEXT NOT NULL DEFAULT 'pending', -- pending|approved|rejected|timed_out
|
|
35
|
+
requested_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
36
|
+
resolved_at TEXT,
|
|
37
|
+
resolved_by TEXT, -- 'operator'|'timeout'|'api'
|
|
38
|
+
notes TEXT
|
|
39
|
+
);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status) WHERE status = 'pending';
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_job ON approvals(job_id);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### schema_migrations: baseline includes these fields/tables
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Function Signatures (contracts between modules)
|
|
49
|
+
|
|
50
|
+
### approval.js (NEW FILE) exports:
|
|
51
|
+
```js
|
|
52
|
+
export function createApproval(jobId, runId) // returns approval record
|
|
53
|
+
export function getApproval(id) // by approval id
|
|
54
|
+
export function getPendingApproval(jobId) // latest pending for a job
|
|
55
|
+
export function listPendingApprovals() // all pending
|
|
56
|
+
export function resolveApproval(id, status, resolvedBy, notes) // approve/reject/timed_out
|
|
57
|
+
export function getTimedOutApprovals() // pending approvals past timeout
|
|
58
|
+
export function pruneApprovals(retentionDays) // clean old resolved approvals
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### retrieval.js (NEW FILE) exports:
|
|
62
|
+
```js
|
|
63
|
+
export function getRecentRunSummaries(jobId, limit) // last N run summaries (non-null)
|
|
64
|
+
export function searchRunSummaries(jobId, query, limit) // hybrid: substring + TF-IDF scoring
|
|
65
|
+
export function buildRetrievalContext(job) // returns string to inject into prompt (or '')
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### jobs.js — add to createJob/updateJob allowed fields:
|
|
69
|
+
delivery_guarantee, job_class, approval_required, approval_timeout_s, approval_auto, context_retrieval, context_retrieval_limit
|
|
70
|
+
|
|
71
|
+
### runs.js — new export:
|
|
72
|
+
```js
|
|
73
|
+
export function updateContextSummary(runId, summaryObj) // store JSON context_summary
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### messages.js — update:
|
|
77
|
+
- sendMessage: accept `owner` field
|
|
78
|
+
- getInbox: sort by typed priority (constraint > decision > fact > task > preference > text/other)
|
|
79
|
+
- New kinds accepted: 'decision','constraint','fact','preference'
|
|
80
|
+
|
|
81
|
+
### dispatcher.js — new functions:
|
|
82
|
+
```js
|
|
83
|
+
async function replayOrphanedRuns() // called from main() after initDb, before tick loop
|
|
84
|
+
async function checkApprovals() // called from tick(), checks timeouts + approved gates
|
|
85
|
+
// Note: context metadata is built inline within buildJobPrompt() and returned as contextMeta
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### cli.js — new commands:
|
|
89
|
+
- `jobs approve <job-id>` — resolve pending approval as approved
|
|
90
|
+
- `jobs reject <job-id> [reason]` — resolve as rejected
|
|
91
|
+
- `approvals list` — list pending
|
|
92
|
+
- `approvals pending` — alias
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Feature Details
|
|
97
|
+
|
|
98
|
+
### F1: Delivery Semantics Contract
|
|
99
|
+
- New field `delivery_guarantee` on jobs ('at-most-once' default | 'at-least-once')
|
|
100
|
+
- at-most-once: current behavior. On crash, orphaned runs marked 'crashed'.
|
|
101
|
+
- at-least-once: on startup, orphaned runs are replayed (new run with replay_of set).
|
|
102
|
+
- Document in job creation. Expose in CLI `jobs list` table.
|
|
103
|
+
|
|
104
|
+
### F2: Flush-Before-Compaction Hook
|
|
105
|
+
- New field `job_class` on jobs ('standard' default | 'pre_compaction_flush')
|
|
106
|
+
- In buildJobPrompt: if job_class === 'pre_compaction_flush', prepend:
|
|
107
|
+
```
|
|
108
|
+
[SYSTEM: Pre-compaction flush required]
|
|
109
|
+
Write a structured summary of: active decisions, constraints, task owners, open questions.
|
|
110
|
+
Format as labeled sections. If nothing needs flushing, respond with exactly: NO_FLUSH
|
|
111
|
+
[END SYSTEM]
|
|
112
|
+
```
|
|
113
|
+
- In dispatch result handling: if content.trim() === 'NO_FLUSH', skip delivery and log 'Flush: nothing to flush'
|
|
114
|
+
|
|
115
|
+
### F3: Context Summary / Memory Observability
|
|
116
|
+
- New field `context_summary` on runs (TEXT, stores JSON)
|
|
117
|
+
- In buildJobPrompt: collect metadata into an object: { messages_injected: N, scope: 'own'|'global', aliases_resolved: [...], job_class, delivery_guarantee, context_retrieval, retrieval_results: N }
|
|
118
|
+
- After creating the run, store the summary via updateContextSummary()
|
|
119
|
+
- Expose in `runs list` and `runs get` CLI output (note: CLI exposure is deferred)
|
|
120
|
+
|
|
121
|
+
### F4: Typed Message Contract
|
|
122
|
+
- New message kinds: 'decision', 'constraint', 'fact', 'preference'
|
|
123
|
+
- New field `owner` on messages
|
|
124
|
+
- In sendMessage: validate kind against full enum, accept owner
|
|
125
|
+
- In getInbox: sort results by typed priority order:
|
|
126
|
+
1. constraint (highest)
|
|
127
|
+
2. decision
|
|
128
|
+
3. fact
|
|
129
|
+
4. task
|
|
130
|
+
5. preference
|
|
131
|
+
6. text, result, status, system, spawn (lowest)
|
|
132
|
+
- In buildJobPrompt: display kind and owner for typed messages:
|
|
133
|
+
```
|
|
134
|
+
[constraint] (owner: ops-agent) Never deploy during business hours
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### F5: HITL Approval Gates
|
|
138
|
+
- New fields on jobs: approval_required (int 0/1), approval_timeout_s (int), approval_auto (text)
|
|
139
|
+
- New run status: 'awaiting_approval'
|
|
140
|
+
- New table: approvals
|
|
141
|
+
- Flow:
|
|
142
|
+
1. In dispatchJob: if job.approval_required AND job is chain-triggered (has parent_id):
|
|
143
|
+
- Create run with status 'awaiting_approval'
|
|
144
|
+
- Create approval record
|
|
145
|
+
- Send notification: "⚠️ Job '{name}' requires approval. Approve: `node cli.js jobs approve {job_id}`"
|
|
146
|
+
- Return (don't dispatch yet)
|
|
147
|
+
2. In tick: call checkApprovals():
|
|
148
|
+
- For each pending approval: check if resolved or timed out
|
|
149
|
+
- If approved: change run status to 'pending', dispatch the job
|
|
150
|
+
- If timed_out: apply approval_auto policy
|
|
151
|
+
- If rejected: mark run as 'cancelled'
|
|
152
|
+
- CLI: approve/reject commands resolve the approval record
|
|
153
|
+
|
|
154
|
+
### F6: Run Replay on Startup
|
|
155
|
+
- New field `replay_of` on runs
|
|
156
|
+
- In main(), after initDb(), before starting tick loop:
|
|
157
|
+
- Query: SELECT r.*, j.delivery_guarantee, j.name FROM runs r JOIN jobs j ON r.job_id = j.id WHERE r.status = 'running'
|
|
158
|
+
- For each orphaned run:
|
|
159
|
+
- If delivery_guarantee = 'at-least-once': create new run with replay_of = old run id, set old run status = 'crashed', queue for dispatch
|
|
160
|
+
- If delivery_guarantee = 'at-most-once': set old run status = 'crashed', advance job schedule
|
|
161
|
+
- Log all actions
|
|
162
|
+
|
|
163
|
+
### F7: Hybrid Retrieval for Job Context
|
|
164
|
+
- New fields on jobs: context_retrieval ('none'|'recent'|'hybrid'), context_retrieval_limit (int)
|
|
165
|
+
- In buildJobPrompt: if context_retrieval !== 'none', call buildRetrievalContext(job) and append to prompt
|
|
166
|
+
- retrieval.js implements:
|
|
167
|
+
- getRecentRunSummaries: SELECT summary FROM runs WHERE job_id=? AND summary IS NOT NULL ORDER BY started_at DESC LIMIT ?
|
|
168
|
+
- searchRunSummaries: combine substring matching + simple TF-IDF scoring
|
|
169
|
+
- TF-IDF: tokenize query and summaries, compute term frequency * inverse document frequency, rank by score
|
|
170
|
+
- buildRetrievalContext: format results as "--- Prior Run Context ---\n[date] summary\n..."
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Installing OpenClaw Scheduler on an Additional Host
|
|
2
|
+
|
|
3
|
+
This guide is for setting up the scheduler on a **second or additional OpenClaw instance**. Each host runs its own independent SQLite database and its own service (launchd on macOS, systemd on Linux, PM2 on Windows) -- they don't share state. This is not a replication setup; each host schedules and dispatches jobs independently.
|
|
4
|
+
|
|
5
|
+
> **Starting fresh:** Unlike migrating from OC cron, on an additional host you'll typically create jobs from scratch. Use the job examples in README.md.
|
|
6
|
+
> **Need copy-paste examples?** See [Starter Recipes in the README](README.md#starter-recipes) and [Common Migrations](README.md#common-migrations).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
| Requirement | Notes |
|
|
13
|
+
|-------------|-------|
|
|
14
|
+
| macOS or Linux | Tested on macOS arm64 |
|
|
15
|
+
| Node.js >= 20 | `node --version` (use full path if needed: `/opt/homebrew/bin/node --version`) |
|
|
16
|
+
| OpenClaw gateway running | With auth token |
|
|
17
|
+
| Git or SCP access | To clone/copy the repo |
|
|
18
|
+
|
|
19
|
+
> **macOS PATH note:** If installed via Homebrew, put the minimal PATH bootstrap in `~/.zshenv`, not only `~/.zprofile`, so non-interactive commands like `ssh host 'node cli.js status'` can find `node` too.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Step 1: Install Scheduler Files
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd ~/.openclaw
|
|
27
|
+
git clone https://github.com/amittell/openclaw-scheduler.git scheduler
|
|
28
|
+
cd scheduler
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or copy from an existing host:
|
|
32
|
+
```bash
|
|
33
|
+
scp -r user@source-host:~/.openclaw/scheduler ~/.openclaw/scheduler
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or npm-first install (no git clone):
|
|
37
|
+
```bash
|
|
38
|
+
mkdir -p ~/.openclaw/scheduler
|
|
39
|
+
npm install --prefix ~/.openclaw/scheduler openclaw-scheduler@latest
|
|
40
|
+
npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- help
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Runtime state for npm installs defaults to `~/.openclaw/scheduler/`, not the package directory under `node_modules/`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Step 2: Install Dependencies
|
|
48
|
+
|
|
49
|
+
If you used the npm-first install path in Step 1, dependencies are already installed; skip to Step 3.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd ~/.openclaw/scheduler
|
|
53
|
+
npm install
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Installs `better-sqlite3` (native, compiles for your arch) and `croner`.
|
|
57
|
+
|
|
58
|
+
If `better-sqlite3` fails: `xcode-select --install` (macOS).
|
|
59
|
+
|
|
60
|
+
If Node changes later on this host, rebuild the native binding before restarting the scheduler:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cd ~/.openclaw/scheduler
|
|
64
|
+
npm rebuild better-sqlite3
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This is especially common after `brew upgrade node` on macOS or any major Node version switch.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Step 2.5: Fix macOS shell PATH and completions
|
|
72
|
+
|
|
73
|
+
If this additional host uses `zsh`, configure shell startup so both interactive terminals and non-interactive remote commands can find Homebrew Node.
|
|
74
|
+
|
|
75
|
+
Recommended `~/.zshenv`:
|
|
76
|
+
|
|
77
|
+
```zsh
|
|
78
|
+
# ~/.zshenv — sourced by all zsh instances, including non-interactive SSH commands
|
|
79
|
+
if [ -x /opt/homebrew/bin/brew ]; then
|
|
80
|
+
eval "$(/opt/homebrew/bin/brew shellenv)"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
export PATH="$HOME/.local/bin:$HOME/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If you load OpenClaw completions in `~/.zshrc`, initialize completions first:
|
|
87
|
+
|
|
88
|
+
```zsh
|
|
89
|
+
autoload -Uz compinit
|
|
90
|
+
compinit
|
|
91
|
+
|
|
92
|
+
if [ -f "$HOME/.openclaw/completions/openclaw.zsh" ]; then
|
|
93
|
+
source "$HOME/.openclaw/completions/openclaw.zsh"
|
|
94
|
+
fi
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Avoid version-pinned Node paths like `/opt/homebrew/opt/node@22/bin`. Prefer `/opt/homebrew/bin/node`.
|
|
98
|
+
|
|
99
|
+
Quick verification:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
ssh "$HOST" 'command -v node && node -v'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Step 3: Run Tests
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
SCHEDULER_DB=:memory: node test.js
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**All tests must pass before proceeding.**
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Step 4: Enable Chat Completions on Gateway
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
openclaw config set gateway.http.endpoints.chatCompletions.enabled true
|
|
121
|
+
openclaw gateway restart
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Verify:
|
|
125
|
+
```bash
|
|
126
|
+
curl -s -o /dev/null -w "%{http_code}" \
|
|
127
|
+
-X POST \
|
|
128
|
+
-H "Authorization: Bearer YOUR_GATEWAY_TOKEN" \
|
|
129
|
+
-H "Content-Type: application/json" \
|
|
130
|
+
-d '{"model":"openclaw:main","messages":[{"role":"user","content":"reply OK"}]}' \
|
|
131
|
+
http://127.0.0.1:18789/v1/chat/completions
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Expected: `200`
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Step 5: Disable OC Built-in Cron
|
|
139
|
+
|
|
140
|
+
If this host had OC built-in cron jobs enabled, disable them so they don't conflict with the scheduler.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
openclaw cron list
|
|
144
|
+
# For each enabled job:
|
|
145
|
+
openclaw cron edit <job-id> --disable
|
|
146
|
+
openclaw config set cron.enabled false
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Also set `OPENCLAW_SKIP_CRON=1` in your OpenClaw gateway service environment (launchctl/systemd/pm2), then restart the gateway.
|
|
150
|
+
|
|
151
|
+
Verify: `openclaw cron list` shows no enabled jobs (or "No cron jobs").
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Step 6: Disable OC Heartbeat
|
|
156
|
+
|
|
157
|
+
If this host had OC heartbeat enabled, disable it:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
openclaw config set agents.defaults.heartbeat.every "0m"
|
|
161
|
+
# If you have per-agent heartbeat overrides, set/remove those too:
|
|
162
|
+
# agents.list[].heartbeat.every = "0m"
|
|
163
|
+
openclaw gateway restart
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Step 7: Choose a macOS launchd mode
|
|
169
|
+
|
|
170
|
+
> **Linux hosts:** For Linux additional hosts, follow the systemd setup in [INSTALL-LINUX.md](INSTALL-LINUX.md) instead of this step.
|
|
171
|
+
|
|
172
|
+
On an additional host, the same choice applies:
|
|
173
|
+
|
|
174
|
+
- **LaunchAgent**: best for a personal Mac with auto-login
|
|
175
|
+
- **LaunchDaemon**: best for a headless host or startup before login
|
|
176
|
+
|
|
177
|
+
Use the setup wizard to install the mode you want:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
cd ~/.openclaw/scheduler
|
|
181
|
+
node setup.mjs --service-mode agent
|
|
182
|
+
# or:
|
|
183
|
+
node setup.mjs --service-mode daemon
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If you installed from npm:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- setup --service-mode agent
|
|
190
|
+
# or:
|
|
191
|
+
npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- setup --service-mode daemon
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Verify the mode you chose:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# LaunchAgent
|
|
198
|
+
launchctl print gui/$UID/ai.openclaw.scheduler
|
|
199
|
+
|
|
200
|
+
# LaunchDaemon
|
|
201
|
+
sudo launchctl print system/ai.openclaw.scheduler
|
|
202
|
+
|
|
203
|
+
# Either mode
|
|
204
|
+
sleep 5 && tail -5 /tmp/openclaw-scheduler.log
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Step 8: Smoke Tests
|
|
210
|
+
|
|
211
|
+
> **Note:** These smoke test commands use direct file imports and are for the git-clone install path. For npm installs, use `openclaw-scheduler` CLI commands instead.
|
|
212
|
+
|
|
213
|
+
### Isolated dispatch
|
|
214
|
+
```bash
|
|
215
|
+
cd ~/.openclaw/scheduler
|
|
216
|
+
node --input-type=module -e "
|
|
217
|
+
import { initDb, getDb } from './db.js';
|
|
218
|
+
import { createJob } from './jobs.js';
|
|
219
|
+
initDb();
|
|
220
|
+
const job = createJob({
|
|
221
|
+
name: 'Smoke Test',
|
|
222
|
+
schedule_cron: '0 0 31 2 *',
|
|
223
|
+
payload_message: 'Reply with exactly: SCHEDULER_OK',
|
|
224
|
+
delivery_mode: 'none',
|
|
225
|
+
delete_after_run: true,
|
|
226
|
+
origin: 'system',
|
|
227
|
+
run_timeout_ms: 300000,
|
|
228
|
+
});
|
|
229
|
+
getDb().prepare(\"UPDATE jobs SET next_run_at = datetime('now', '-1 second') WHERE id = ?\").run(job.id);
|
|
230
|
+
console.log('Created smoke test:', job.id);
|
|
231
|
+
"
|
|
232
|
+
sleep 20 && tail -10 /tmp/openclaw-scheduler.log
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Look for: `Dispatching: Smoke Test` → `Completed: Smoke Test`
|
|
236
|
+
|
|
237
|
+
### Telegram delivery
|
|
238
|
+
```bash
|
|
239
|
+
node --input-type=module -e "
|
|
240
|
+
import { initDb, getDb } from './db.js';
|
|
241
|
+
import { createJob } from './jobs.js';
|
|
242
|
+
initDb();
|
|
243
|
+
const job = createJob({
|
|
244
|
+
name: 'Telegram Test',
|
|
245
|
+
schedule_cron: '0 0 31 2 *',
|
|
246
|
+
payload_message: 'Confirm scheduler is working. Send a brief greeting.',
|
|
247
|
+
delivery_mode: 'announce',
|
|
248
|
+
delivery_channel: 'telegram',
|
|
249
|
+
delivery_to: 'YOUR_CHAT_ID',
|
|
250
|
+
delete_after_run: true,
|
|
251
|
+
origin: 'system',
|
|
252
|
+
run_timeout_ms: 300000,
|
|
253
|
+
});
|
|
254
|
+
getDb().prepare(\"UPDATE jobs SET next_run_at = datetime('now', '-1 second') WHERE id = ?\").run(job.id);
|
|
255
|
+
console.log('Created Telegram test:', job.id);
|
|
256
|
+
"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
You should receive a Telegram message within 30 seconds.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Step 9: Create Your Jobs
|
|
264
|
+
|
|
265
|
+
Since this is a fresh host, create jobs from scratch using the CLI:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
node cli.js jobs add '{
|
|
269
|
+
"name": "My First Job",
|
|
270
|
+
"schedule_cron": "0 * * * *",
|
|
271
|
+
"payload_message": "Run your task here",
|
|
272
|
+
"delivery_mode": "announce",
|
|
273
|
+
"delivery_channel": "telegram",
|
|
274
|
+
"delivery_to": "YOUR_CHAT_ID"
|
|
275
|
+
}'
|
|
276
|
+
node cli.js jobs list
|
|
277
|
+
node cli.js status
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
See README.md for full job examples including shell jobs, workflow chains, approval gates, and more.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Step 10: Verify First Real Job
|
|
285
|
+
|
|
286
|
+
Wait for the next scheduled job and confirm:
|
|
287
|
+
```bash
|
|
288
|
+
tail -f /tmp/openclaw-scheduler.log
|
|
289
|
+
# Or after it fires:
|
|
290
|
+
node cli.js runs list <job-id>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Rollback
|
|
296
|
+
|
|
297
|
+
If anything goes wrong:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# 1. Stop scheduler
|
|
301
|
+
launchctl bootout gui/$UID/ai.openclaw.scheduler # if using LaunchAgent
|
|
302
|
+
sudo launchctl bootout system/ai.openclaw.scheduler # if using LaunchDaemon
|
|
303
|
+
|
|
304
|
+
# 2. Re-enable OC cron (if you disabled it)
|
|
305
|
+
openclaw cron edit <job-id> --enable # for each job
|
|
306
|
+
openclaw config set cron.enabled true
|
|
307
|
+
# remove OPENCLAW_SKIP_CRON=1 from gateway service env
|
|
308
|
+
|
|
309
|
+
# 3. Re-enable heartbeat (if you disabled it)
|
|
310
|
+
openclaw config set agents.defaults.heartbeat.every "5m"
|
|
311
|
+
openclaw gateway restart
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Validation Checklist
|
|
317
|
+
|
|
318
|
+
- [ ] `SCHEDULER_DB=:memory: node test.js` -- all passing, 0 failed
|
|
319
|
+
- [ ] `node cli.js status` → shows jobs, 0 stale
|
|
320
|
+
- [ ] `launchctl print gui/$UID/ai.openclaw.scheduler` or `sudo launchctl print system/ai.openclaw.scheduler` → running
|
|
321
|
+
- [ ] Log file has startup lines, no errors
|
|
322
|
+
- [ ] OC cron → all disabled (if applicable)
|
|
323
|
+
- [ ] OC heartbeat → `0m` (if applicable)
|
|
324
|
+
- [ ] Chat completions → 200
|
|
325
|
+
- [ ] Smoke test → dispatched + completed in log
|
|
326
|
+
- [ ] Telegram test → message received
|
|
327
|
+
- [ ] First real job → fires on schedule
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Upgrading
|
|
332
|
+
|
|
333
|
+
Already have the scheduler installed and need to update to a newer version? See [UPGRADING.md](UPGRADING.md).
|