openclaw-scheduler 0.2.13 → 0.2.14
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 +36 -1
- package/BEST-PRACTICES.md +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/dispatch/README.md +17 -2
- package/dispatch/chilisaus.config.example.json +10 -0
- package/dispatch/completion.mjs +21 -46
- package/dispatch/default-model.mjs +83 -0
- package/dispatch/index.mjs +7 -1
- package/dispatch/watcher.mjs +61 -27
- package/package.json +4 -2
package/AGENTS.md
CHANGED
|
@@ -35,7 +35,7 @@ Check-in messages are delivered asynchronously. By the time they appear, the job
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
# For chilisaus-dispatched jobs:
|
|
38
|
-
node ~/.openclaw/chilisaus
|
|
38
|
+
node ~/.openclaw/worktrees/openclaw-scheduler/dispatch/chilisaus.mjs status --label <label>
|
|
39
39
|
|
|
40
40
|
# For scheduler jobs:
|
|
41
41
|
openclaw-scheduler runs list <job-id> --json
|
|
@@ -321,3 +321,38 @@ See the
|
|
|
321
321
|
for agentcli-specific agent instructions and the
|
|
322
322
|
[MANIFEST-QUICK-REF.md](https://github.com/amittell/agentcli/blob/main/MANIFEST-QUICK-REF.md)
|
|
323
323
|
for copy-paste manifest patterns.
|
|
324
|
+
|
|
325
|
+
<!-- coord:begin -->
|
|
326
|
+
<!-- AUTO-GENERATED by 'coord upgrade'. Do not hand-edit; next upgrade will overwrite. -->
|
|
327
|
+
## Coordination protocol (mandatory)
|
|
328
|
+
|
|
329
|
+
Multi-agent file coordination via the `coord` MCP server. Required calls every session:
|
|
330
|
+
|
|
331
|
+
1. `list_claims` at task start.
|
|
332
|
+
2. `claim_files` before editing.
|
|
333
|
+
3. `release_claims` (or `release_session` to drop everything this MCP session holds, v0.6+) when done.
|
|
334
|
+
|
|
335
|
+
If `claim_files` returns conflicts, stop and ask the user. No edits outside claimed scope; no opportunistic refactors; shared config edits only with explicit user approval.
|
|
336
|
+
|
|
337
|
+
### Sub-file (symbol-level) claims (v0.14+)
|
|
338
|
+
|
|
339
|
+
When two agents need different parts of the same file, claim symbols instead of whole files. Pass `symbols={"src/auth/login.ts": ["handleLogin"]}` to `claim_files` so the claim scopes to just that function. Two agents on disjoint symbols of the same file auto-coexist with no 409.
|
|
340
|
+
|
|
341
|
+
- `Foo::handleA` (v0.16): method-level notation. Sibling methods of the same class auto-coexist; the bare class blocks all of its methods.
|
|
342
|
+
- `Outer::Inner::method` (v0.17): recursive nesting works to any depth.
|
|
343
|
+
- Server-side validation (v0.17): when `COORD_REPO_ROOT` is set, the server rejects symbols that do not exist in the file with a hint listing what is parseable. The MCP wrapper pre-validates locally before POST so typos fail fast; disable with `COORD_DISABLE_CLIENT_VALIDATION=1`.
|
|
344
|
+
|
|
345
|
+
### Queueing instead of bouncing (v0.21+)
|
|
346
|
+
|
|
347
|
+
When a hot file is contested and your work is blocking the team, pass `wait_seconds=60` to `claim_files`. The request joins a FIFO queue behind the blocking holder; on release the next queued requester is auto-granted. Pair with `urgency='low' | 'normal' | 'high' | 'blocking'` (v0.25) to jump ahead of lower-priority waiters; long-waiting entries age-boost one level after `COORD_QUEUE_AGE_BOOST_SECONDS` (v0.26). Abandon a wait early via `cancel_queue_request(queue_id, engineer=...)` (v0.26). Inspect your own queue rows via `my_requests(queued=True)` (v0.22).
|
|
348
|
+
|
|
349
|
+
### Asking the holder directly (v0.6+ / v0.11+ decisions)
|
|
350
|
+
|
|
351
|
+
If queueing will not work either:
|
|
352
|
+
- `request_release` files an explicit ask against the holder's claim. The holder's TTL shortens; their decision lands back in your `my_requests` view.
|
|
353
|
+
- `respond_to_request` decisions (v0.11+): `approved` (release whole claim), `denied` (keep it), `narrowed` (close the claim, open a tighter one -- pass `narrowed_pattern`), `coexist` (let the requester have a sibling claim on the same scope -- pass `coexist_pattern`). `coexist` is cooperative not enforced; agents on the same file still handle imports and module-level state themselves.
|
|
354
|
+
|
|
355
|
+
### Operator visibility
|
|
356
|
+
|
|
357
|
+
Poll `pending_requests` between operations to see who is blocked on your scope and respond via `respond_to_request`. The dashboard surfaces hotspot files, an auto-resolution heatmap, and a pending-queue panel for ambient awareness.
|
|
358
|
+
<!-- coord:end -->
|
package/BEST-PRACTICES.md
CHANGED
|
@@ -368,7 +368,7 @@ Check-in messages (from `agent-checkin.mjs` or similar) are delivered asynchrono
|
|
|
368
368
|
|
|
369
369
|
```bash
|
|
370
370
|
# Always do this before reporting job status:
|
|
371
|
-
node ~/.openclaw/chilisaus
|
|
371
|
+
node ~/.openclaw/worktrees/openclaw-scheduler/dispatch/chilisaus.mjs status --label <label>
|
|
372
372
|
```
|
|
373
373
|
|
|
374
374
|
The `status` output gives you the authoritative `status` field (`accepted` / `running` / `done` / `error`), the last `updatedAt` timestamp, and the final `summary`. Use that — not the most recent check-in message.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.14] -- 2026-06-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- fix(dispatch): honor configured dispatch default models from wrapper config, `DISPATCH_DEFAULT_MODEL`, and OpenClaw agent defaults before falling back to the static default
|
|
9
|
+
- fix(dispatch): verify interrupted sessions before marking watcher failures
|
|
10
|
+
- fix(dispatch): prefer human completion summaries in delivery flows
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- chore: upgrade coord to v0.35.0
|
|
14
|
+
- docs(dispatch): retire old chilisaus fork paths and document migrated dispatch configuration
|
|
15
|
+
|
|
5
16
|
## [0.2.13] -- 2026-06-24
|
|
6
17
|
|
|
7
18
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1767,7 +1767,7 @@ For normal chat-triggered dispatches, always pass `--deliver-to` from the inboun
|
|
|
1767
1767
|
| `--message-stdin` | -- | Read the prompt from stdin explicitly. If stdin is piped and no explicit prompt source is set, dispatch auto-reads stdin. |
|
|
1768
1768
|
| `--mode` | `fresh` | `fresh` creates a new session. `reuse` continues the last session recorded for this label. |
|
|
1769
1769
|
| `--thinking` | -- | Reasoning budget: `low`, `high`, or `xhigh`. |
|
|
1770
|
-
| `--model` |
|
|
1770
|
+
| `--model` | configured dispatch default | Model override, e.g. `anthropic/claude-sonnet-4-6`. When omitted, dispatch uses wrapper `config.defaultModel`, wrapper `config.dispatch.model`, `DISPATCH_DEFAULT_MODEL`, `agents.defaults.dispatch.model`, `agents.defaults.model`, then the built-in fallback. |
|
|
1771
1771
|
| `--deliver-to` | -- | Delivery target (e.g. Telegram chat ID). Registers the scheduler watcher job for durable final delivery. The gateway spawn itself stays fire-and-forget so raw tool output and internal done payloads cannot leak directly to chat. Chat-triggered callers should pass inbound metadata `chat_id` here, especially for group chats. |
|
|
1772
1772
|
| `--delivery-mode` | `announce` | `announce` delivers only when output is non-empty. `announce-always` delivers unconditionally. `none` suppresses delivery. |
|
|
1773
1773
|
| `--timeout` | `300` | Session timeout in seconds. |
|
package/dispatch/README.md
CHANGED
|
@@ -20,8 +20,9 @@ No scheduler DB dependency. No dispatcher tick delay. Sessions start instantly.
|
|
|
20
20
|
| `watcher.mjs` | Delivery monitoring process |
|
|
21
21
|
| `529-recovery.mjs` | Transient error recovery |
|
|
22
22
|
| `deliver-watcher.sh` | Shell wrapper for result retrieval |
|
|
23
|
-
| `chilisaus.mjs` | Branded wrapper |
|
|
23
|
+
| `chilisaus.mjs` | Branded chilisaus wrapper over this dispatch engine |
|
|
24
24
|
| `config.example.json` | Example config |
|
|
25
|
+
| `chilisaus.config.example.json` | Chilisaus branding config example |
|
|
25
26
|
| `test-done-postoffice.mjs` | Done handler test |
|
|
26
27
|
| `~/.openclaw/scheduler/dispatch/labels.json` | Durable label→session ledger |
|
|
27
28
|
| `README.md` | This file |
|
|
@@ -50,6 +51,20 @@ Orchestrator calls:
|
|
|
50
51
|
→ hooks.mjs fires dispatch.started to Loki
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
### Chilisaus branding
|
|
55
|
+
|
|
56
|
+
Chilisaus is not a separate implementation. It is the branded entrypoint in this
|
|
57
|
+
directory:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
node dispatch/chilisaus.mjs enqueue --label ticket-42 --message "Fix it"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`chilisaus.mjs` sets `DISPATCH_CONFIG_DIR` to this dispatch directory, then loads
|
|
64
|
+
`index.mjs`. To use the historical chilisaus branding, copy
|
|
65
|
+
`chilisaus.config.example.json` to `config.json` in the same directory. Runtime
|
|
66
|
+
changes belong in this `openclaw-scheduler/dispatch` tree.
|
|
67
|
+
|
|
53
68
|
---
|
|
54
69
|
|
|
55
70
|
## Subcommands
|
|
@@ -86,7 +101,7 @@ cat prompt.md | node dispatch/index.mjs enqueue \
|
|
|
86
101
|
| `--mode` | `fresh` | `fresh` = new session; `reuse` = continue last session for this label |
|
|
87
102
|
| `--session-key` | — | Explicit session key (bypasses ledger lookup) |
|
|
88
103
|
| `--agent` | `main` | Agent ID |
|
|
89
|
-
| `--model` |
|
|
104
|
+
| `--model` | configured dispatch default | Model override (e.g. `anthropic/claude-sonnet-4-6`). When omitted, dispatch uses wrapper `config.defaultModel`, wrapper `config.dispatch.model`, `DISPATCH_DEFAULT_MODEL`, `agents.defaults.dispatch.model`, `agents.defaults.model`, then the built-in fallback. |
|
|
90
105
|
| `--thinking` | — | Reasoning level: `low`, `high`, `xhigh` |
|
|
91
106
|
| `--timeout` | `300` | Seconds before run times out |
|
|
92
107
|
| `--deliver-to` | — | Delivery target (chat ID, channel ID, handle, etc.). Enables `deliver:true` on the gateway call. Chat-triggered callers should pass inbound metadata `chat_id` here, especially for group chats. |
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Chilisaus branding config. Copy to config.json next to dispatch/chilisaus.mjs for a branded deployment.",
|
|
3
|
+
|
|
4
|
+
"name": "chilisaus",
|
|
5
|
+
"brand": "chilisaus 🌶️",
|
|
6
|
+
|
|
7
|
+
"startupGraceMs": 90000,
|
|
8
|
+
"stuckThresholdMs": 600000,
|
|
9
|
+
"maxWatcherAgeMs": 7200000
|
|
10
|
+
}
|
package/dispatch/completion.mjs
CHANGED
|
@@ -39,7 +39,6 @@ const HUMAN_SUMMARY_SECTION_RE = /(?:^|\n)\s*(?:Human-readable summary|Human sum
|
|
|
39
39
|
const TECHNICAL_DETAILS_SECTION_RE = /(?:^|\n)\s*(?:Technical details?|Details(?:_technical)?)\s*:\s*/i;
|
|
40
40
|
const HUMAN_SUMMARY_LABEL_RE = /^(?:human-readable summary|human summary)\s*:\s*/i;
|
|
41
41
|
const TECHNICAL_DETAILS_LABEL_RE = /^(?:technical details?|details(?:_technical)?)\s*:\s*/i;
|
|
42
|
-
const FORMATTER_META_SUMMARY_RE = /^Final completion updates now (?:start with a short plain-English summary|arrive as one clean plain-English summary)\./i;
|
|
43
42
|
const FINAL_REPORT_HEADING_RE = /^(?:#{1,6}\s*)?(?:root cause|files? changed|changes|validation|tests?(?: run| passed)?|sacrificial(?: delivery)?(?: result)?|deployment(?:\/live-runtime)?(?: step)?|live-runtime(?: step)?|result|results|summary|highlights?|notes?|follow[- ]ups?|next steps?|blockers?|implementation|what changed|verification)\s*:?$/i;
|
|
44
43
|
const FINAL_REPORT_CUE_RE = /\b(?:root cause|files? changed|tests? run|validation|sacrificial(?: delivery)?(?: result)?|deployment(?:\/live-runtime)?(?: step)?|live-runtime(?: step)?|final report|human-readable report|files changed|tests passed)\b/i;
|
|
45
44
|
|
|
@@ -752,42 +751,12 @@ function buildCompletionLeadFromThemes(themes) {
|
|
|
752
751
|
return truncateText(sentences.join(' '), MAX_DELIVERY_CHARS);
|
|
753
752
|
}
|
|
754
753
|
|
|
755
|
-
function looksLikeFormatterMetaSummary(text) {
|
|
756
|
-
const normalized = normalizeCompletionText(text);
|
|
757
|
-
return Boolean(normalized && FORMATTER_META_SUMMARY_RE.test(normalized));
|
|
758
|
-
}
|
|
759
|
-
|
|
760
754
|
function getCompletionRawSummary(completion) {
|
|
761
755
|
const details = getCompletionTechnicalDetails(completion);
|
|
762
756
|
if (!details || typeof details !== 'object') return null;
|
|
763
757
|
return normalizeCompletionText(details.raw_summary ?? details.rawSummary);
|
|
764
758
|
}
|
|
765
759
|
|
|
766
|
-
function looksLikeCompletionFormatterImplementation(text) {
|
|
767
|
-
const normalized = normalizeCompletionText(text);
|
|
768
|
-
if (!normalized) return false;
|
|
769
|
-
|
|
770
|
-
const fragments = extractTechnicalFragments(normalized);
|
|
771
|
-
const themes = detectTechnicalThemes(normalized, fragments);
|
|
772
|
-
if (!themes.completionFlow) return false;
|
|
773
|
-
|
|
774
|
-
return TECHNICAL_COMMIT_PREFIX_RE.test(cleanMarkdown(normalized).replace(/\s+/g, ' ').trim())
|
|
775
|
-
|| /\b(?:completion|deliveryText|summary_human|summaryHuman|details_technical|resolveCompletionDelivery|buildTerminalCompletionPayload|payload-precedence|watcher path|completion watcher|final completion updates?)\b/i.test(normalized);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function getRawSummaryOverrideForFormatterMeta(completion, structuredTexts) {
|
|
779
|
-
if (!structuredTexts.some(looksLikeFormatterMetaSummary)) return null;
|
|
780
|
-
|
|
781
|
-
const rawSummary = getCompletionRawSummary(completion);
|
|
782
|
-
if (!rawSummary) return null;
|
|
783
|
-
if (isGenericOrTrivial(rawSummary)) return null;
|
|
784
|
-
if (isInternalTransportNoiseText(rawSummary)) return null;
|
|
785
|
-
if (looksLikeRawPayloadText(rawSummary)) return null;
|
|
786
|
-
if (looksLikeCompletionFormatterImplementation(rawSummary)) return null;
|
|
787
|
-
|
|
788
|
-
return truncateText(rawSummary, MAX_DELIVERY_CHARS);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
760
|
function buildHumanizedTechnicalSummary(rawText, fallbackSummary) {
|
|
792
761
|
const cleanedRaw = normalizeCompletionText(rawText);
|
|
793
762
|
const fallback = normalizeCompletionText(fallbackSummary);
|
|
@@ -888,7 +857,7 @@ function summarizeChecklistTechnicalDetails(checklist, sha) {
|
|
|
888
857
|
return parts.length > 0 ? `Checks: ${parts.join('; ')}.` : null;
|
|
889
858
|
}
|
|
890
859
|
|
|
891
|
-
function buildTechnicalDetailsText({ rawText, summaryText, completion } = {}) {
|
|
860
|
+
function buildTechnicalDetailsText({ rawText, summaryText, completion, includeRawSummaryDetails = true } = {}) {
|
|
892
861
|
const raw = normalizeCompletionText(rawText);
|
|
893
862
|
const summary = normalizeCompletionText(summaryText);
|
|
894
863
|
const details = getCompletionTechnicalDetails(completion);
|
|
@@ -922,7 +891,7 @@ function buildTechnicalDetailsText({ rawText, summaryText, completion } = {}) {
|
|
|
922
891
|
&& (!rawTechnical || normalized !== rawTechnicalSource)) {
|
|
923
892
|
parts.push(truncateText(normalized, 220));
|
|
924
893
|
}
|
|
925
|
-
} else if (details && typeof details === 'object') {
|
|
894
|
+
} else if (includeRawSummaryDetails && details && typeof details === 'object') {
|
|
926
895
|
const rawSummary = normalizeCompletionText(details.raw_summary);
|
|
927
896
|
const detailSummarySections = extractStructuredSummarySections(rawSummary);
|
|
928
897
|
const splitDetailSummary = extractExplicitTechnicalTail(rawSummary);
|
|
@@ -1019,7 +988,7 @@ export function isMeaningfulCompletionText(value) {
|
|
|
1019
988
|
}
|
|
1020
989
|
|
|
1021
990
|
function getCompletionSummaryHuman(completion) {
|
|
1022
|
-
return normalizeCompletionText(completion?.summary_human ?? completion?.summaryHuman);
|
|
991
|
+
return normalizeCompletionText(completion?.summary_human ?? completion?.summaryHuman ?? completion?.prose);
|
|
1023
992
|
}
|
|
1024
993
|
|
|
1025
994
|
function getCompletionTechnicalDetails(completion) {
|
|
@@ -1328,12 +1297,14 @@ export function resolveCompletionDelivery({ lastReply, completion, fallbackSumma
|
|
|
1328
1297
|
const rawCompletionSummaryHuman = getCompletionSummaryHuman(completion);
|
|
1329
1298
|
const rawCompletionSummary = normalizeCompletionText(completion?.summary);
|
|
1330
1299
|
const rawCompletionDelivery = normalizeCompletionText(completion?.deliveryText);
|
|
1300
|
+
const rawCompletionRawSummary = getCompletionRawSummary(completion);
|
|
1331
1301
|
const rawFallback = normalizeCompletionText(fallbackSummary);
|
|
1332
1302
|
|
|
1333
1303
|
const reply = humanizeCompletionText(lastReply);
|
|
1334
1304
|
const completionSummaryHuman = humanizeCompletionText(rawCompletionSummaryHuman);
|
|
1335
1305
|
const completionSummary = humanizeCompletionText(completion?.summary);
|
|
1336
1306
|
const completionDelivery = humanizeCompletionText(completion?.deliveryText);
|
|
1307
|
+
const completionRawSummary = humanizeCompletionText(rawCompletionRawSummary);
|
|
1337
1308
|
const fallback = humanizeCompletionText(fallbackSummary);
|
|
1338
1309
|
const synthesizedFromTechnical = synthesizeCompletionReply({
|
|
1339
1310
|
checklist: completion?.checklist,
|
|
@@ -1344,23 +1315,12 @@ export function resolveCompletionDelivery({ lastReply, completion, fallbackSumma
|
|
|
1344
1315
|
const authoritativeStructuredSummary = completionDeliverySource && completionDeliverySource !== 'technical-synthesis'
|
|
1345
1316
|
? preferredSummary
|
|
1346
1317
|
: null;
|
|
1347
|
-
const rawSummaryOverride = getRawSummaryOverrideForFormatterMeta(completion, [
|
|
1348
|
-
rawCompletionSummaryHuman,
|
|
1349
|
-
rawCompletionSummary,
|
|
1350
|
-
rawCompletionDelivery,
|
|
1351
|
-
]);
|
|
1352
|
-
if (rawSummaryOverride) {
|
|
1353
|
-
return {
|
|
1354
|
-
deliveryText: rawSummaryOverride,
|
|
1355
|
-
summary: rawSummaryOverride,
|
|
1356
|
-
source: 'raw-summary',
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
1318
|
const noisyTexts = [
|
|
1360
1319
|
rawReply,
|
|
1361
1320
|
rawCompletionSummaryHuman,
|
|
1362
1321
|
rawCompletionDelivery,
|
|
1363
1322
|
rawCompletionSummary,
|
|
1323
|
+
rawCompletionRawSummary,
|
|
1364
1324
|
rawFallback,
|
|
1365
1325
|
].filter(isInternalTransportNoiseText);
|
|
1366
1326
|
const isDeliverableText = (rawText, summarizedText) => Boolean(summarizedText)
|
|
@@ -1395,6 +1355,7 @@ export function resolveCompletionDelivery({ lastReply, completion, fallbackSumma
|
|
|
1395
1355
|
rawText: candidate.rawText,
|
|
1396
1356
|
summaryText: candidate.text,
|
|
1397
1357
|
completion,
|
|
1358
|
+
includeRawSummaryDetails: false,
|
|
1398
1359
|
});
|
|
1399
1360
|
return {
|
|
1400
1361
|
deliveryText: composeDeliveryText(candidate.text, technicalDetailsText),
|
|
@@ -1408,6 +1369,7 @@ export function resolveCompletionDelivery({ lastReply, completion, fallbackSumma
|
|
|
1408
1369
|
rawText: rawReply,
|
|
1409
1370
|
summaryText: reply,
|
|
1410
1371
|
completion,
|
|
1372
|
+
includeRawSummaryDetails: false,
|
|
1411
1373
|
});
|
|
1412
1374
|
return {
|
|
1413
1375
|
deliveryText: composeDeliveryText(reply, technicalDetailsText),
|
|
@@ -1416,6 +1378,19 @@ export function resolveCompletionDelivery({ lastReply, completion, fallbackSumma
|
|
|
1416
1378
|
};
|
|
1417
1379
|
}
|
|
1418
1380
|
|
|
1381
|
+
if (isDeliverableText(rawCompletionRawSummary, completionRawSummary)) {
|
|
1382
|
+
const technicalDetailsText = buildTechnicalDetailsText({
|
|
1383
|
+
rawText: rawCompletionRawSummary,
|
|
1384
|
+
summaryText: completionRawSummary,
|
|
1385
|
+
completion,
|
|
1386
|
+
});
|
|
1387
|
+
return {
|
|
1388
|
+
deliveryText: composeDeliveryText(completionRawSummary, technicalDetailsText),
|
|
1389
|
+
summary: completionRawSummary,
|
|
1390
|
+
source: 'raw-summary',
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1419
1394
|
if (isDeliverableText(synthesizedFromTechnical, synthesizedFromTechnical)) {
|
|
1420
1395
|
return {
|
|
1421
1396
|
deliveryText: synthesizedFromTechnical,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, resolve as pathResolve } from 'path';
|
|
3
|
+
|
|
4
|
+
export const STATIC_DISPATCH_DEFAULT_MODEL = 'openai/gpt-5.5';
|
|
5
|
+
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeString(value) {
|
|
11
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveModelPrimary(value) {
|
|
15
|
+
const direct = normalizeString(value);
|
|
16
|
+
if (direct) return direct;
|
|
17
|
+
if (!isRecord(value)) return null;
|
|
18
|
+
return normalizeString(value.primary);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveHomeRelativePath(input, homeDir) {
|
|
22
|
+
const trimmed = normalizeString(input);
|
|
23
|
+
if (!trimmed) return null;
|
|
24
|
+
if (trimmed === '~') return homeDir;
|
|
25
|
+
if (trimmed.startsWith('~/')) return join(homeDir, trimmed.slice(2));
|
|
26
|
+
return pathResolve(trimmed);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveOpenClawStateDir({ env, homeDir }) {
|
|
30
|
+
return resolveHomeRelativePath(env.OPENCLAW_STATE_DIR, homeDir) || join(homeDir, '.openclaw');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveOpenClawConfigPath({ env = process.env, homeDir } = {}) {
|
|
34
|
+
const effectiveHome = homeDir || env.HOME || process.env.HOME;
|
|
35
|
+
if (!effectiveHome) return null;
|
|
36
|
+
return (
|
|
37
|
+
resolveHomeRelativePath(env.OPENCLAW_CONFIG_PATH, effectiveHome) ||
|
|
38
|
+
join(resolveOpenClawStateDir({ env, homeDir: effectiveHome }), 'openclaw.json')
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readOpenClawConfig({
|
|
43
|
+
env = process.env,
|
|
44
|
+
homeDir,
|
|
45
|
+
exists = existsSync,
|
|
46
|
+
readFile = readFileSync,
|
|
47
|
+
} = {}) {
|
|
48
|
+
const configPath = resolveOpenClawConfigPath({ env, homeDir });
|
|
49
|
+
if (!configPath || !exists(configPath)) return {};
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFile(configPath, 'utf8'));
|
|
52
|
+
} catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveOpenClawDispatchDefaultModel(config) {
|
|
58
|
+
const defaults = isRecord(config?.agents?.defaults) ? config.agents.defaults : {};
|
|
59
|
+
return (
|
|
60
|
+
resolveModelPrimary(defaults.dispatch?.model) ||
|
|
61
|
+
resolveModelPrimary(defaults.model) ||
|
|
62
|
+
null
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveDefaultDispatchModel({
|
|
67
|
+
dispatchConfig = {},
|
|
68
|
+
openClawConfig,
|
|
69
|
+
env = process.env,
|
|
70
|
+
homeDir,
|
|
71
|
+
exists = existsSync,
|
|
72
|
+
readFile = readFileSync,
|
|
73
|
+
} = {}) {
|
|
74
|
+
return (
|
|
75
|
+
normalizeString(dispatchConfig.defaultModel) ||
|
|
76
|
+
resolveModelPrimary(dispatchConfig.dispatch?.model) ||
|
|
77
|
+
normalizeString(env.DISPATCH_DEFAULT_MODEL) ||
|
|
78
|
+
resolveOpenClawDispatchDefaultModel(
|
|
79
|
+
openClawConfig ?? readOpenClawConfig({ env, homeDir, exists, readFile }),
|
|
80
|
+
) ||
|
|
81
|
+
STATIC_DISPATCH_DEFAULT_MODEL
|
|
82
|
+
);
|
|
83
|
+
}
|
package/dispatch/index.mjs
CHANGED
|
@@ -45,6 +45,7 @@ import { getDispatchLivenessPolicy } from './liveness.mjs';
|
|
|
45
45
|
import { resolveLabelsPath } from './paths.mjs';
|
|
46
46
|
import { onStarted, onFinished, onStuck } from './hooks.mjs';
|
|
47
47
|
import { resolveMessageInput } from './message-input.mjs';
|
|
48
|
+
import { resolveDefaultDispatchModel } from './default-model.mjs';
|
|
48
49
|
import { buildDispatchDeliverySurface } from '../scripts/dispatch-cli-utils.mjs';
|
|
49
50
|
|
|
50
51
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -96,6 +97,11 @@ function loadConfig() {
|
|
|
96
97
|
|
|
97
98
|
const config = loadConfig();
|
|
98
99
|
const BRAND = config.name ?? 'dispatch';
|
|
100
|
+
const DEFAULT_DISPATCH_MODEL = resolveDefaultDispatchModel({
|
|
101
|
+
dispatchConfig: config,
|
|
102
|
+
env: process.env,
|
|
103
|
+
homeDir: HOME_DIR,
|
|
104
|
+
});
|
|
99
105
|
|
|
100
106
|
/** Load gateway auth token from config or env */
|
|
101
107
|
function getGatewayToken() {
|
|
@@ -1096,7 +1102,7 @@ async function cmdEnqueue(flags) {
|
|
|
1096
1102
|
|
|
1097
1103
|
// Dynamic branding: resolve per-agent brand name
|
|
1098
1104
|
const agentBrand = config.agents?.[agent]?.name || (agent !== 'main' ? agent : null) || config.name || 'dispatch';
|
|
1099
|
-
const model = flags.model ||
|
|
1105
|
+
const model = flags.model || DEFAULT_DISPATCH_MODEL;
|
|
1100
1106
|
|
|
1101
1107
|
// -- Session key resolution ----------------------------------
|
|
1102
1108
|
let sessionKey = flags['session-key'] || null;
|
package/dispatch/watcher.mjs
CHANGED
|
@@ -927,6 +927,32 @@ function markLabelError(label, errorSummary) {
|
|
|
927
927
|
}
|
|
928
928
|
}
|
|
929
929
|
|
|
930
|
+
function getLabelEntry(label) {
|
|
931
|
+
try {
|
|
932
|
+
const labels = loadLabels();
|
|
933
|
+
return labels[label] || null;
|
|
934
|
+
} catch (err) {
|
|
935
|
+
process.stderr.write(`[watcher] label load failed for ${label}: ${err.message}\n`);
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function runVerifyCmd(label, entry) {
|
|
941
|
+
if (!entry?.verifyCmd) return { configured: false, ok: false };
|
|
942
|
+
|
|
943
|
+
process.stderr.write(`[watcher] running verify-cmd for ${label}: ${entry.verifyCmd}\n`);
|
|
944
|
+
try {
|
|
945
|
+
execSync(entry.verifyCmd, { stdio: 'pipe', timeout: 60000, shell: true });
|
|
946
|
+
process.stderr.write(`[watcher] verify-cmd passed for ${label}\n`);
|
|
947
|
+
return { configured: true, ok: true };
|
|
948
|
+
} catch (verifyErr) {
|
|
949
|
+
const stderr = verifyErr.stderr ? verifyErr.stderr.toString().trim() : verifyErr.message;
|
|
950
|
+
const message = stderr || `exit code ${verifyErr.status ?? 1}`;
|
|
951
|
+
process.stderr.write(`[watcher] verify-cmd failed: ${message}\n`);
|
|
952
|
+
return { configured: true, ok: false, message };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
930
956
|
let exitZeroOnTerminal = false;
|
|
931
957
|
|
|
932
958
|
/**
|
|
@@ -941,33 +967,20 @@ function deliverResult(label, lastReply, fallbackSummary, completionPayload = nu
|
|
|
941
967
|
// -- verify-cmd check -----------------------------------------------------
|
|
942
968
|
// Run the stored verify-cmd (if any) before declaring the job done.
|
|
943
969
|
// A non-zero exit flips the job to error state and sends an alert instead.
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
process.stdout.write(
|
|
959
|
-
`🌶️ *dispatch* [${label}] ⚠️ VERIFICATION FAILED\n\n` +
|
|
960
|
-
`The agent session completed but the post-completion verify-cmd exited non-zero.\n\n` +
|
|
961
|
-
`**Verify command:** \`${entry.verifyCmd}\`\n` +
|
|
962
|
-
`**Error:** ${stderr || 'non-zero exit'}\n\n` +
|
|
963
|
-
`Job marked as \`error\`. The agent may have reported done without completing the actual work.\n`
|
|
964
|
-
);
|
|
965
|
-
process.exit(exitZeroOnTerminal ? 0 : 1);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
} catch (loadErr) {
|
|
969
|
-
// Non-fatal -- if labels can't be read, skip verify check and proceed normally
|
|
970
|
-
process.stderr.write(`[watcher] verify-cmd check skipped (labels load error): ${loadErr.message}\n`);
|
|
970
|
+
const entry = getLabelEntry(label);
|
|
971
|
+
const verify = runVerifyCmd(label, entry);
|
|
972
|
+
if (verify.configured && !verify.ok) {
|
|
973
|
+
const errMsg = `verify-cmd failed: ${verify.message || 'non-zero exit'}`;
|
|
974
|
+
markLabelError(label, errMsg);
|
|
975
|
+
// Output failure notice -- scheduler delivers this to the delivery target
|
|
976
|
+
process.stdout.write(
|
|
977
|
+
`🌶️ *dispatch* [${label}] ⚠️ VERIFICATION FAILED\n\n` +
|
|
978
|
+
`The agent session completed but the post-completion verify-cmd exited non-zero.\n\n` +
|
|
979
|
+
`**Verify command:** \`${entry.verifyCmd}\`\n` +
|
|
980
|
+
`**Error:** ${verify.message || 'non-zero exit'}\n\n` +
|
|
981
|
+
`Job marked as \`error\`. The agent may have reported done without completing the actual work.\n`
|
|
982
|
+
);
|
|
983
|
+
process.exit(exitZeroOnTerminal ? 0 : 1);
|
|
971
984
|
}
|
|
972
985
|
|
|
973
986
|
// Update labels.json before exiting -- prevents stuck detector false positives
|
|
@@ -995,6 +1008,27 @@ function deliverResult(label, lastReply, fallbackSummary, completionPayload = nu
|
|
|
995
1008
|
|
|
996
1009
|
function emitInterruptedOutcome(label, summary, result = null) {
|
|
997
1010
|
process.stderr.write(`[watcher] [${label}] session auto-resolved as interrupted -- work may be incomplete\n`);
|
|
1011
|
+
const entry = getLabelEntry(label);
|
|
1012
|
+
const verify = runVerifyCmd(label, entry);
|
|
1013
|
+
if (verify.configured && verify.ok) {
|
|
1014
|
+
const recoveredReply = result?.lastReply || result?.diagnosticReply || null;
|
|
1015
|
+
const completionPayload = result?.completion || (recoveredReply ? null : {
|
|
1016
|
+
summary_human: summary
|
|
1017
|
+
? `The session stopped before sending the done signal, but verification passed. ${summary}`
|
|
1018
|
+
: 'The session stopped before sending the done signal, but verification passed.',
|
|
1019
|
+
details: {
|
|
1020
|
+
interrupted: true,
|
|
1021
|
+
verify_cmd: entry.verifyCmd,
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
deliverResult(
|
|
1025
|
+
label,
|
|
1026
|
+
recoveredReply,
|
|
1027
|
+
'completed after interrupted session; verify-cmd passed',
|
|
1028
|
+
completionPayload,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
998
1032
|
markLabelError(label, summary || 'interrupted: session went idle without calling done');
|
|
999
1033
|
process.stdout.write(
|
|
1000
1034
|
`⚠️ dispatch [${label}] session went idle before completing -- work may be incomplete` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-scheduler",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
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": "22.x || 24.x || 26.x"
|
|
20
|
+
"node": "22.x || 24.x || 25.x || 26.x"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"start": "node dispatcher.js",
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"dispatch/529-recovery.mjs",
|
|
40
40
|
"dispatch/completion.mjs",
|
|
41
41
|
"dispatch/config.example.json",
|
|
42
|
+
"dispatch/chilisaus.config.example.json",
|
|
43
|
+
"dispatch/default-model.mjs",
|
|
42
44
|
"dispatch/deliver-watcher.sh",
|
|
43
45
|
"dispatch/hooks.mjs",
|
|
44
46
|
"dispatch/index.mjs",
|