openclaw-scheduler 0.2.12 → 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 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/index.mjs status --label <label>
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/index.mjs status --label <label>
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,22 @@
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
+
16
+ ## [0.2.13] -- 2026-06-24
17
+
18
+ ### Fixed
19
+ - fix(dispatch): prefer structured completion payloads for chilisaus status/result/list summaries before falling back to generic label summaries or transcript text
20
+
5
21
  ## [0.2.12] -- 2026-06-23
6
22
 
7
23
  ### Changed
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` | -- | Model override, e.g. `anthropic/claude-sonnet-4-6`. |
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. |
@@ -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` | | Model override (e.g. `anthropic/claude-sonnet-4-6`) |
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
+ }
@@ -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
+ }
@@ -38,12 +38,14 @@ import {
38
38
  extractLastMeaningfulAssistantReplyFromEntries,
39
39
  extractTerminalAssistantReplyFromEntries,
40
40
  hasCompletionSignal,
41
+ resolveCompletionDelivery,
41
42
  taskRequiresGitSha,
42
43
  } from './completion.mjs';
43
44
  import { getDispatchLivenessPolicy } from './liveness.mjs';
44
45
  import { resolveLabelsPath } from './paths.mjs';
45
46
  import { onStarted, onFinished, onStuck } from './hooks.mjs';
46
47
  import { resolveMessageInput } from './message-input.mjs';
48
+ import { resolveDefaultDispatchModel } from './default-model.mjs';
47
49
  import { buildDispatchDeliverySurface } from '../scripts/dispatch-cli-utils.mjs';
48
50
 
49
51
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -95,6 +97,11 @@ function loadConfig() {
95
97
 
96
98
  const config = loadConfig();
97
99
  const BRAND = config.name ?? 'dispatch';
100
+ const DEFAULT_DISPATCH_MODEL = resolveDefaultDispatchModel({
101
+ dispatchConfig: config,
102
+ env: process.env,
103
+ homeDir: HOME_DIR,
104
+ });
98
105
 
99
106
  /** Load gateway auth token from config or env */
100
107
  function getGatewayToken() {
@@ -302,6 +309,33 @@ function setLabelDone(name, data) {
302
309
  return labels[name];
303
310
  }
304
311
 
312
+ function effectiveCompletionSummary(entry, lastReply = null) {
313
+ if (!entry || typeof entry !== 'object') return null;
314
+
315
+ if (hasCompletionSignal(entry.completion)) {
316
+ const resolved = resolveCompletionDelivery({
317
+ lastReply,
318
+ completion: entry.completion || null,
319
+ fallbackSummary: entry.summary || null,
320
+ });
321
+ if (resolved?.summary) return resolved.summary;
322
+ }
323
+
324
+ if (entry.summary) return entry.summary;
325
+
326
+ if (lastReply) {
327
+ const resolved = resolveCompletionDelivery({
328
+ lastReply,
329
+ completion: null,
330
+ fallbackSummary: null,
331
+ });
332
+ if (resolved?.summary) return resolved.summary;
333
+ return lastReply.slice(0, 500);
334
+ }
335
+
336
+ return null;
337
+ }
338
+
305
339
  // -- Gateway Calls --------------------------------------------
306
340
 
307
341
  /**
@@ -1068,7 +1102,7 @@ async function cmdEnqueue(flags) {
1068
1102
 
1069
1103
  // Dynamic branding: resolve per-agent brand name
1070
1104
  const agentBrand = config.agents?.[agent]?.name || (agent !== 'main' ? agent : null) || config.name || 'dispatch';
1071
- const model = flags.model || null;
1105
+ const model = flags.model || DEFAULT_DISPATCH_MODEL;
1072
1106
 
1073
1107
  // -- Session key resolution ----------------------------------
1074
1108
  let sessionKey = flags['session-key'] || null;
@@ -1573,7 +1607,7 @@ function cmdStatus(flags) {
1573
1607
  status: current.status,
1574
1608
  spawnedAt: current.spawnedAt,
1575
1609
  updatedAt: current.updatedAt,
1576
- summary: current.summary || null,
1610
+ summary: effectiveCompletionSummary(current),
1577
1611
  completion: current.completion || null,
1578
1612
  delivery: buildDispatchDeliverySurface(current),
1579
1613
  error: current.error || null,
@@ -1919,7 +1953,7 @@ function cmdResult(flags) {
1919
1953
  sessionKey: entry.sessionKey,
1920
1954
  status: entry.status,
1921
1955
  spawnedAt: entry.spawnedAt,
1922
- summary: entry.summary || (lastReply ? lastReply.slice(0, 500) : null),
1956
+ summary: effectiveCompletionSummary(entry, lastReply),
1923
1957
  completion: entry.completion || null,
1924
1958
  delivery: buildDispatchDeliverySurface(entry),
1925
1959
  lastReply: lastReply || null,
@@ -2349,6 +2383,7 @@ function cmdList(flags) {
2349
2383
  let entries = Object.entries(labels).map(([name, data]) => ({
2350
2384
  label: name,
2351
2385
  ...data,
2386
+ summary: effectiveCompletionSummary(data),
2352
2387
  delivery: buildDispatchDeliverySurface(data),
2353
2388
  }));
2354
2389
 
@@ -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
- try {
945
- const labels = loadLabels();
946
- const entry = labels[label];
947
- if (entry?.verifyCmd) {
948
- process.stderr.write(`[watcher] running verify-cmd for ${label}: ${entry.verifyCmd}\n`);
949
- try {
950
- execSync(entry.verifyCmd, { stdio: 'pipe', timeout: 60000, shell: true });
951
- process.stderr.write(`[watcher] verify-cmd passed for ${label}\n`);
952
- } catch (verifyErr) {
953
- const stderr = verifyErr.stderr ? verifyErr.stderr.toString().trim() : verifyErr.message;
954
- const errMsg = `verify-cmd failed: ${stderr || 'exit code ' + (verifyErr.status ?? 1)}`;
955
- process.stderr.write(`[watcher] ${errMsg}\n`);
956
- markLabelError(label, errMsg);
957
- // Output failure notice -- scheduler delivers this to the delivery target
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.12",
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",