openclaw-scheduler 0.2.6 → 0.2.8
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/dispatch/529-recovery.mjs +2 -1
- package/dispatch/README.md +20 -6
- package/dispatch/index.mjs +27 -19
- package/dispatch/liveness.mjs +61 -0
- package/dispatch/watcher.mjs +33 -17
- package/package.json +2 -1
- package/scripts/stuck-run-detector.mjs +2 -1
|
@@ -23,9 +23,10 @@ import { readFileSync, writeFileSync, renameSync } from 'fs';
|
|
|
23
23
|
import { execFileSync } from 'child_process';
|
|
24
24
|
import { dirname, join } from 'path';
|
|
25
25
|
import { fileURLToPath } from 'url';
|
|
26
|
+
import { resolveLabelsPath } from './paths.mjs';
|
|
26
27
|
|
|
27
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
const LABELS_PATH =
|
|
29
|
+
const LABELS_PATH = resolveLabelsPath({ legacyCandidates: [join(__dirname, 'labels.json')] });
|
|
29
30
|
const INDEX_PATH = process.env.DISPATCH_INDEX_PATH || join(__dirname, 'index.mjs');
|
|
30
31
|
|
|
31
32
|
const MAX_RETRIES = 3;
|
package/dispatch/README.md
CHANGED
|
@@ -23,7 +23,7 @@ No scheduler DB dependency. No dispatcher tick delay. Sessions start instantly.
|
|
|
23
23
|
| `chilisaus.mjs` | Branded wrapper |
|
|
24
24
|
| `config.example.json` | Example config |
|
|
25
25
|
| `test-done-postoffice.mjs` | Done handler test |
|
|
26
|
-
|
|
|
26
|
+
| `~/.openclaw/scheduler/dispatch/labels.json` | Durable label→session ledger |
|
|
27
27
|
| `README.md` | This file |
|
|
28
28
|
|
|
29
29
|
---
|
|
@@ -45,7 +45,7 @@ Orchestrator calls:
|
|
|
45
45
|
→ Patches session with model/thinking/spawnDepth
|
|
46
46
|
→ Calls gateway `agent` method with the task
|
|
47
47
|
→ Session starts immediately (no scheduler tick delay)
|
|
48
|
-
→ Tracks label→sessionKey in labels
|
|
48
|
+
→ Tracks label→sessionKey in the durable labels ledger
|
|
49
49
|
→ Agent auto-announces results on completion
|
|
50
50
|
→ hooks.mjs fires dispatch.started to Loki
|
|
51
51
|
```
|
|
@@ -117,7 +117,7 @@ node dispatch/index.mjs stuck --threshold-min 15
|
|
|
117
117
|
Exit 0 = nothing stuck (silent).
|
|
118
118
|
Exit 1 = stuck sessions found (triggers announce delivery).
|
|
119
119
|
|
|
120
|
-
Checks labels
|
|
120
|
+
Checks the labels ledger for sessions marked `running`, cross-references gateway
|
|
121
121
|
session store for last activity timestamp.
|
|
122
122
|
|
|
123
123
|
### `result` — last assistant reply from a session
|
|
@@ -206,7 +206,7 @@ Shows all labels in the ledger, sorted by most recent. Filter by status.
|
|
|
206
206
|
node dispatch/index.mjs sync
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
Reconciles
|
|
209
|
+
Reconciles the labels ledger with the gateway sessions store. Sessions that no
|
|
210
210
|
longer exist on the gateway are marked stale, and sessions present on the
|
|
211
211
|
gateway but missing from the ledger are imported. Useful after gateway restarts
|
|
212
212
|
or manual session cleanup.
|
|
@@ -291,8 +291,8 @@ No manual token configuration needed on a standard OpenClaw install.
|
|
|
291
291
|
|
|
292
292
|
When `--deliver-to` is set, dispatch registers a **scheduler watcher job**
|
|
293
293
|
after dispatching the session. The watcher polls the session result every
|
|
294
|
-
minute until the agent
|
|
295
|
-
`handleDelivery` pipeline.
|
|
294
|
+
minute until the agent sends the structured `done` completion signal, then
|
|
295
|
+
delivers via the scheduler's `handleDelivery` pipeline.
|
|
296
296
|
|
|
297
297
|
```
|
|
298
298
|
dispatch enqueue --deliver-to <telegram-user-id>
|
|
@@ -316,6 +316,20 @@ dispatch enqueue --deliver-to <telegram-user-id>
|
|
|
316
316
|
Exit 1 with no output = retry on next cron tick (no spam — `announce-always`
|
|
317
317
|
only delivers when `output.trim()` is truthy).
|
|
318
318
|
|
|
319
|
+
Quiet sessions are treated conservatively. The watcher does not mark a running
|
|
320
|
+
job failed just because `sessions.json` or the JSONL transcript has been quiet
|
|
321
|
+
for 60 seconds. For high/xhigh reasoning work, the first idle result probe waits
|
|
322
|
+
at least 10 minutes, idle auto-resolution waits at least 20 minutes, and the hard
|
|
323
|
+
failure ceiling is longer than the requested task timeout. Missing or ambiguous
|
|
324
|
+
gateway/session liveness fails open to "still monitoring" until the hard timeout
|
|
325
|
+
window or a clear terminal error.
|
|
326
|
+
|
|
327
|
+
While a label is still `running`, a plain assistant reply is diagnostic only.
|
|
328
|
+
Successful final delivery requires the agent-side `done` signal and its
|
|
329
|
+
structured completion payload. If an older watcher records an error and the
|
|
330
|
+
worker later sends a valid `done`, the later completion is authoritative and the
|
|
331
|
+
stale error is cleared from the label.
|
|
332
|
+
|
|
319
333
|
### Progress check-ins from subagent sessions
|
|
320
334
|
|
|
321
335
|
Subagent sessions run without PATH access to the `openclaw` CLI, so
|
package/dispatch/index.mjs
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
hasCompletionSignal,
|
|
41
41
|
taskRequiresGitSha,
|
|
42
42
|
} from './completion.mjs';
|
|
43
|
+
import { getDispatchLivenessPolicy } from './liveness.mjs';
|
|
44
|
+
import { resolveLabelsPath } from './paths.mjs';
|
|
43
45
|
import { onStarted, onFinished, onStuck } from './hooks.mjs';
|
|
44
46
|
import { resolveMessageInput } from './message-input.mjs';
|
|
45
47
|
import { buildDispatchDeliverySurface } from '../scripts/dispatch-cli-utils.mjs';
|
|
@@ -66,7 +68,9 @@ const INVOKE_DIR = (() => {
|
|
|
66
68
|
|
|
67
69
|
// -- Config ---------------------------------------------------
|
|
68
70
|
|
|
69
|
-
const LABELS_PATH =
|
|
71
|
+
const LABELS_PATH = resolveLabelsPath({
|
|
72
|
+
legacyCandidates: [join(INVOKE_DIR, 'labels.json'), join(__dirname, 'labels.json')],
|
|
73
|
+
});
|
|
70
74
|
|
|
71
75
|
/** Load dispatch config from config.json.
|
|
72
76
|
* Resolution order:
|
|
@@ -211,9 +215,9 @@ function setLabelDone(name, data) {
|
|
|
211
215
|
...current[name],
|
|
212
216
|
...data,
|
|
213
217
|
status: 'done',
|
|
218
|
+
error: null,
|
|
214
219
|
updatedAt: new Date().toISOString(),
|
|
215
220
|
};
|
|
216
|
-
delete current[name].error;
|
|
217
221
|
});
|
|
218
222
|
return labels[name];
|
|
219
223
|
}
|
|
@@ -1311,16 +1315,17 @@ function cmdStatus(flags) {
|
|
|
1311
1315
|
//
|
|
1312
1316
|
// PING_STALE_MS: 3x the 60s ping interval -- if we haven't heard from the
|
|
1313
1317
|
// watcher in 3 min, it's probably dead; fall through to check.
|
|
1314
|
-
// hardCeilingMs:
|
|
1315
|
-
//
|
|
1316
|
-
// idleThresholdMs:
|
|
1317
|
-
//
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const
|
|
1318
|
+
// hardCeilingMs: timeout/reasoning-aware hard ceiling. High-thinking
|
|
1319
|
+
// work gets a larger quiet window before hard failure.
|
|
1320
|
+
// idleThresholdMs: timeout/reasoning-aware quiet threshold. Ambiguous or
|
|
1321
|
+
// missing liveness stays running until these thresholds.
|
|
1322
|
+
const livenessPolicy = getDispatchLivenessPolicy(entry, {
|
|
1323
|
+
startupGraceMs: STARTUP_GRACE_MS,
|
|
1324
|
+
defaultTimeoutSeconds: 600,
|
|
1325
|
+
});
|
|
1326
|
+
const PING_STALE_MS = livenessPolicy.pingStaleMs;
|
|
1327
|
+
const idleThresholdMs = livenessPolicy.idleFailureMs;
|
|
1328
|
+
const hardCeilingMs = livenessPolicy.hardCeilingMs;
|
|
1324
1329
|
|
|
1325
1330
|
let check;
|
|
1326
1331
|
if (ageMs < STARTUP_GRACE_MS) {
|
|
@@ -1333,13 +1338,13 @@ function cmdStatus(flags) {
|
|
|
1333
1338
|
check = { shouldResolve: false };
|
|
1334
1339
|
} else {
|
|
1335
1340
|
// Ping stale OR past hard ceiling: fall through to session store check
|
|
1336
|
-
const thresh = ageMs >= hardCeilingMs ?
|
|
1341
|
+
const thresh = ageMs >= hardCeilingMs ? livenessPolicy.hardTimeoutIdleMs : idleThresholdMs;
|
|
1337
1342
|
check = checkSessionDone(entry.sessionKey, sessionsStore, thresh, true, spawnedAtMs);
|
|
1338
1343
|
}
|
|
1339
1344
|
} else {
|
|
1340
1345
|
// No lastPing -- backward compat (sessions dispatched before heartbeat feature).
|
|
1341
1346
|
// Use idleThresholdMs (job-aware) instead of the old hardcoded 10 min.
|
|
1342
|
-
const thresh = ageMs >= hardCeilingMs ?
|
|
1347
|
+
const thresh = ageMs >= hardCeilingMs ? livenessPolicy.hardTimeoutIdleMs : idleThresholdMs;
|
|
1343
1348
|
check = checkSessionDone(entry.sessionKey, sessionsStore, thresh, true, spawnedAtMs);
|
|
1344
1349
|
}
|
|
1345
1350
|
|
|
@@ -1616,10 +1621,13 @@ function cmdSync(flags) {
|
|
|
1616
1621
|
// -- Heartbeat-based liveness guard (mirrors cmdStatus logic) ---------
|
|
1617
1622
|
// Skip auto-resolve when the watcher's lastPing heartbeat is fresh.
|
|
1618
1623
|
// See cmdStatus for full commentary on PING_STALE_MS / hardCeilingMs.
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1624
|
+
const syncPolicy = getDispatchLivenessPolicy(entry, {
|
|
1625
|
+
startupGraceMs: STARTUP_GRACE_MS_SYNC,
|
|
1626
|
+
defaultTimeoutSeconds: 600,
|
|
1627
|
+
});
|
|
1628
|
+
const PING_STALE_MS_SYNC = syncPolicy.pingStaleMs;
|
|
1629
|
+
const idleThresholdMsSync = syncPolicy.idleFailureMs;
|
|
1630
|
+
const hardCeilingMsSync = syncPolicy.hardCeilingMs;
|
|
1623
1631
|
|
|
1624
1632
|
if (entry.lastPing) {
|
|
1625
1633
|
const pingAgeMs = Date.now() - new Date(entry.lastPing).getTime();
|
|
@@ -1629,7 +1637,7 @@ function cmdSync(flags) {
|
|
|
1629
1637
|
}
|
|
1630
1638
|
}
|
|
1631
1639
|
|
|
1632
|
-
const syncThresh = elapsedMs >= hardCeilingMsSync ?
|
|
1640
|
+
const syncThresh = elapsedMs >= hardCeilingMsSync ? syncPolicy.hardTimeoutIdleMs : idleThresholdMsSync;
|
|
1633
1641
|
const check = checkSessionDone(entry.sessionKey, syncStore, syncThresh, true, spawnedAtMs);
|
|
1634
1642
|
|
|
1635
1643
|
if (check.shouldResolve) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const MINUTE_MS = 60 * 1000;
|
|
2
|
+
|
|
3
|
+
function numberOrNull(value) {
|
|
4
|
+
const n = Number(value);
|
|
5
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeThinkingLevel(value) {
|
|
9
|
+
const text = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
10
|
+
if (text === 'xhigh' || text === 'extra-high' || text === 'extra_high') return 'xhigh';
|
|
11
|
+
if (text === 'high') return 'high';
|
|
12
|
+
if (text === 'low') return 'low';
|
|
13
|
+
if (text === 'off' || text === 'none') return 'off';
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getDispatchTimeoutSeconds(entry = {}, fallbackSeconds = 300) {
|
|
18
|
+
return numberOrNull(entry.timeoutSeconds)
|
|
19
|
+
?? numberOrNull(entry.timeout)
|
|
20
|
+
?? numberOrNull(fallbackSeconds)
|
|
21
|
+
?? 300;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getDispatchLivenessPolicy(entry = {}, opts = {}) {
|
|
25
|
+
const now = numberOrNull(opts.now) ?? Date.now();
|
|
26
|
+
const timeoutSeconds = getDispatchTimeoutSeconds(entry, opts.defaultTimeoutSeconds);
|
|
27
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
28
|
+
const thinking = normalizeThinkingLevel(entry.thinking);
|
|
29
|
+
const isHighThinking = thinking === 'high' || thinking === 'xhigh';
|
|
30
|
+
|
|
31
|
+
const startupGraceMs = numberOrNull(opts.startupGraceMs)
|
|
32
|
+
?? (isHighThinking ? 10 * MINUTE_MS : 5 * MINUTE_MS);
|
|
33
|
+
const pingStaleMs = numberOrNull(opts.pingStaleMs) ?? 3 * MINUTE_MS;
|
|
34
|
+
const idleProbeFloorMs = isHighThinking ? 10 * MINUTE_MS : 1 * MINUTE_MS;
|
|
35
|
+
const idleProbeMs = Math.max(
|
|
36
|
+
idleProbeFloorMs,
|
|
37
|
+
Math.min(timeoutMs * 0.25, isHighThinking ? 15 * MINUTE_MS : 5 * MINUTE_MS),
|
|
38
|
+
);
|
|
39
|
+
const idleFailureFloorMs = isHighThinking ? 20 * MINUTE_MS : 10 * MINUTE_MS;
|
|
40
|
+
const idleFailureMs = Math.max(timeoutMs, idleFailureFloorMs);
|
|
41
|
+
const hardCeilingMs = Math.max(timeoutMs * 1.5, idleFailureMs * (isHighThinking ? 2 : 1.5));
|
|
42
|
+
const hardTimeoutIdleMs = isHighThinking ? 5 * MINUTE_MS : 2 * MINUTE_MS;
|
|
43
|
+
const spawnedAtMs = entry.spawnedAt ? new Date(entry.spawnedAt).getTime() : 0;
|
|
44
|
+
const ageMs = spawnedAtMs ? now - spawnedAtMs : Infinity;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
thinking,
|
|
48
|
+
isHighThinking,
|
|
49
|
+
timeoutSeconds,
|
|
50
|
+
timeoutMs,
|
|
51
|
+
startupGraceMs,
|
|
52
|
+
pingStaleMs,
|
|
53
|
+
idleProbeMs,
|
|
54
|
+
idleFailureMs,
|
|
55
|
+
hardCeilingMs,
|
|
56
|
+
hardTimeoutIdleMs,
|
|
57
|
+
spawnedAtMs,
|
|
58
|
+
ageMs,
|
|
59
|
+
pastHardCeiling: ageMs >= hardCeilingMs,
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dispatch/watcher.mjs
CHANGED
|
@@ -36,11 +36,13 @@ import {
|
|
|
36
36
|
hasCompletionSignal,
|
|
37
37
|
resolveCompletionDelivery,
|
|
38
38
|
} from './completion.mjs';
|
|
39
|
+
import { getDispatchLivenessPolicy } from './liveness.mjs';
|
|
40
|
+
import { resolveLabelsPath } from './paths.mjs';
|
|
39
41
|
import { sendMessage } from '../messages.js';
|
|
40
42
|
|
|
41
43
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
42
44
|
const INDEX_PATH = process.env.DISPATCH_INDEX_PATH || join(__dirname, 'index.mjs');
|
|
43
|
-
const LABELS_PATH =
|
|
45
|
+
const LABELS_PATH = resolveLabelsPath({ legacyCandidates: [join(__dirname, 'labels.json')] });
|
|
44
46
|
const HOME_DIR = process.env.HOME || homedir();
|
|
45
47
|
let labelsCache = null;
|
|
46
48
|
let labelsCacheSignature = null;
|
|
@@ -988,8 +990,14 @@ const pollS = parseInt(flags['poll-interval'] || '20', 10);
|
|
|
988
990
|
const once = flags.once === true || flags.once === 'true';
|
|
989
991
|
exitZeroOnTerminal = once;
|
|
990
992
|
|
|
991
|
-
|
|
992
|
-
const
|
|
993
|
+
function getCurrentLivenessPolicy() {
|
|
994
|
+
const entry = loadLabels()[label] || { timeoutSeconds: timeoutS };
|
|
995
|
+
return getDispatchLivenessPolicy(entry, { defaultTimeoutSeconds: timeoutS });
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function hasStructuredCompletion(result) {
|
|
999
|
+
return hasCompletionSignal(result?.completion);
|
|
1000
|
+
}
|
|
993
1001
|
|
|
994
1002
|
if (!label) {
|
|
995
1003
|
process.stderr.write('[watcher] --label is required\n');
|
|
@@ -1104,18 +1112,22 @@ function runOnceAndExit() {
|
|
|
1104
1112
|
const terminalJsonlReply = sessionId ? getSessionTerminalReply(sessionId, sessionAgent) : null;
|
|
1105
1113
|
if (sessionId && terminalJsonlReply && isSessionCleanlyFinished(sessionId, sessionAgent)) {
|
|
1106
1114
|
const result = dispatch('result', ['--label', label]);
|
|
1107
|
-
|
|
1115
|
+
if (hasStructuredCompletion(result)) {
|
|
1116
|
+
deliverResult(label, result?.lastReply || terminalJsonlReply, 'completed (stop_reason=end_turn)', result?.completion || null);
|
|
1117
|
+
}
|
|
1118
|
+
process.stderr.write(`[watcher] stop_reason=end_turn observed without completion signal -- continuing to monitor\n`);
|
|
1108
1119
|
}
|
|
1109
1120
|
}
|
|
1110
1121
|
|
|
1111
1122
|
const ageMs = status.liveness?.ageMs;
|
|
1112
|
-
|
|
1123
|
+
const idleResultCheckMs = getCurrentLivenessPolicy().idleProbeMs;
|
|
1124
|
+
if (ageMs != null && ageMs >= idleResultCheckMs) {
|
|
1113
1125
|
const result = dispatch('result', ['--label', label]);
|
|
1114
|
-
if (
|
|
1126
|
+
if (hasStructuredCompletion(result)) {
|
|
1115
1127
|
deliverResult(label, result?.lastReply || null, null, result?.completion || null);
|
|
1116
1128
|
}
|
|
1117
1129
|
|
|
1118
|
-
const stallReason = getRunningSessionStallReason(status,
|
|
1130
|
+
const stallReason = getRunningSessionStallReason(status, idleResultCheckMs);
|
|
1119
1131
|
if (stallReason) {
|
|
1120
1132
|
process.stderr.write(`[watcher] [${label}] ${stallReason}\n`);
|
|
1121
1133
|
markLabelError(label, stallReason);
|
|
@@ -1477,8 +1489,11 @@ while (Date.now() < deadline) {
|
|
|
1477
1489
|
if (_sid2a && terminalJsonlReply && isSessionCleanlyFinished(_sid2a, _adir2a)) {
|
|
1478
1490
|
process.stderr.write(`[watcher] stop_reason=end_turn detected -- delivering early\n`);
|
|
1479
1491
|
const result = dispatch('result', ['--label', label]);
|
|
1480
|
-
|
|
1481
|
-
|
|
1492
|
+
if (hasStructuredCompletion(result)) {
|
|
1493
|
+
deliverResult(label, result?.lastReply || terminalJsonlReply, 'completed (stop_reason=end_turn)', result?.completion || null);
|
|
1494
|
+
// deliverResult exits
|
|
1495
|
+
}
|
|
1496
|
+
process.stderr.write(`[watcher] stop_reason=end_turn observed without completion signal -- continuing to monitor\n`);
|
|
1482
1497
|
}
|
|
1483
1498
|
}
|
|
1484
1499
|
|
|
@@ -1489,13 +1504,14 @@ while (Date.now() < deadline) {
|
|
|
1489
1504
|
// while this watcher's lastPing heartbeat is fresh (written every 60s);
|
|
1490
1505
|
// this path handles normal completion before the ping goes stale.
|
|
1491
1506
|
const ageMs = status.liveness?.ageMs;
|
|
1492
|
-
|
|
1507
|
+
const idleResultCheckMs = getCurrentLivenessPolicy().idleProbeMs;
|
|
1508
|
+
if (ageMs != null && ageMs >= idleResultCheckMs) {
|
|
1493
1509
|
const result = dispatch('result', ['--label', label]);
|
|
1494
|
-
if (
|
|
1510
|
+
if (hasStructuredCompletion(result)) {
|
|
1495
1511
|
deliverResult(label, result?.lastReply || null, null, result?.completion || null);
|
|
1496
1512
|
}
|
|
1497
1513
|
|
|
1498
|
-
const stallReason = getRunningSessionStallReason(status,
|
|
1514
|
+
const stallReason = getRunningSessionStallReason(status, idleResultCheckMs);
|
|
1499
1515
|
if (stallReason) {
|
|
1500
1516
|
process.stderr.write(`[watcher] [${label}] ${stallReason}\n`);
|
|
1501
1517
|
markLabelError(label, stallReason);
|
|
@@ -1577,7 +1593,7 @@ if (sessionInternalId) {
|
|
|
1577
1593
|
// If the session already completed (gateway pruned it -> null tokens), exit cleanly.
|
|
1578
1594
|
if (statusAtDeadline?.status === 'done' || baselineTokens === null) {
|
|
1579
1595
|
const r = dispatch('result', ['--label', label]);
|
|
1580
|
-
if (
|
|
1596
|
+
if (hasStructuredCompletion(r)) {
|
|
1581
1597
|
// deliverResult calls process.exit(0) internally
|
|
1582
1598
|
deliverResult(label, r?.lastReply || null, statusAtDeadline?.summary || null, r?.completion || null);
|
|
1583
1599
|
}
|
|
@@ -1616,7 +1632,7 @@ while (Date.now() - flatSince < FLAT_WINDOW_MS) {
|
|
|
1616
1632
|
deliverResult(label, r?.lastReply || null, st.summary, r?.completion || st?.completion || null);
|
|
1617
1633
|
}
|
|
1618
1634
|
const r2 = dispatch('result', ['--label', label]);
|
|
1619
|
-
if (
|
|
1635
|
+
if (hasStructuredCompletion(r2)) {
|
|
1620
1636
|
// deliverResult calls process.exit(0) internally
|
|
1621
1637
|
deliverResult(label, r2?.lastReply || null, null, r2?.completion || null);
|
|
1622
1638
|
}
|
|
@@ -1710,7 +1726,7 @@ if (sessionInternalId) {
|
|
|
1710
1726
|
deliverResult(label, rExt?.lastReply || null, stExt.summary, rExt?.completion || stExt?.completion || null);
|
|
1711
1727
|
}
|
|
1712
1728
|
const rExt2 = dispatch('result', ['--label', label]);
|
|
1713
|
-
if (
|
|
1729
|
+
if (hasStructuredCompletion(rExt2)) {
|
|
1714
1730
|
// deliverResult calls process.exit(0) internally
|
|
1715
1731
|
deliverResult(label, rExt2?.lastReply || null, null, rExt2?.completion || null);
|
|
1716
1732
|
}
|
|
@@ -1767,7 +1783,7 @@ for (const round of steerRounds) {
|
|
|
1767
1783
|
deliverResult(label, r3?.lastReply || null, st2.summary, r3?.completion || st2?.completion || null);
|
|
1768
1784
|
}
|
|
1769
1785
|
const r3 = dispatch('result', ['--label', label]);
|
|
1770
|
-
if (
|
|
1786
|
+
if (hasStructuredCompletion(r3)) {
|
|
1771
1787
|
// deliverResult calls process.exit(0) internally
|
|
1772
1788
|
deliverResult(label, r3?.lastReply || null, null, r3?.completion || null);
|
|
1773
1789
|
}
|
|
@@ -1782,7 +1798,7 @@ for (const round of steerRounds) {
|
|
|
1782
1798
|
if (st3?.status === 'done') {
|
|
1783
1799
|
// Check if a result was captured before marking as error
|
|
1784
1800
|
const r4 = dispatch('result', ['--label', label]);
|
|
1785
|
-
if (
|
|
1801
|
+
if (hasStructuredCompletion(r4)) {
|
|
1786
1802
|
deliverResult(label, r4?.lastReply || null, st3.summary, r4?.completion || st3?.completion || null); // deliverResult calls process.exit(0)
|
|
1787
1803
|
}
|
|
1788
1804
|
markLabelError(label, 'timed out -- killed after steer attempts (no result captured)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-scheduler",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "SQLite-backed job scheduler and workflow engine for OpenClaw agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"dispatch/deliver-watcher.sh",
|
|
43
43
|
"dispatch/hooks.mjs",
|
|
44
44
|
"dispatch/index.mjs",
|
|
45
|
+
"dispatch/liveness.mjs",
|
|
45
46
|
"dispatch/message-input.mjs",
|
|
46
47
|
"dispatch/README.md",
|
|
47
48
|
"dispatch/watcher.mjs",
|
|
@@ -29,12 +29,13 @@ import { join, dirname } from 'path';
|
|
|
29
29
|
import { fileURLToPath } from 'url';
|
|
30
30
|
import { tmpdir } from 'os';
|
|
31
31
|
import { resolveDispatchCliPath, resolveDispatchLabel } from './dispatch-cli-utils.mjs';
|
|
32
|
+
import { resolveLabelsPath } from '../dispatch/paths.mjs';
|
|
32
33
|
|
|
33
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
35
|
|
|
35
36
|
// -- Paths ----------------------------------------------------
|
|
36
37
|
|
|
37
|
-
const LABELS_PATH =
|
|
38
|
+
const LABELS_PATH = resolveLabelsPath({ legacyCandidates: [join(__dirname, '..', 'dispatch', 'labels.json')] });
|
|
38
39
|
const STATE_PATH = process.env.STUCK_STATE_PATH || join(tmpdir(), 'stuck-detector-state.json');
|
|
39
40
|
const DISPATCH_CLI = resolveDispatchCliPath(process.env);
|
|
40
41
|
const DISPATCH_IS_BIN = !DISPATCH_CLI.includes('/') && !DISPATCH_CLI.includes('\\');
|