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.
@@ -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 = process.env.DISPATCH_LABELS_PATH || join(__dirname, 'labels.json');
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;
@@ -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
- | `labels.json` | Local label→session ledger (gitignored) |
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.json
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.json for sessions marked `running`, cross-references gateway
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 `labels.json` with the gateway sessions store. Sessions that no
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 produces a reply, then delivers via the scheduler's
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
@@ -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 = process.env.DISPATCH_LABELS_PATH || join(INVOKE_DIR, 'labels.json');
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: job timeout * 1.5 -- absolute max regardless of ping age.
1315
- // Catches zombie watchers (watcher alive but session is stuck).
1316
- // idleThresholdMs: max(job timeout, 10 min) -- replaces the old hardcoded 10-min
1317
- // threshold so longer jobs aren't killed at exactly 10 min.
1318
- const PING_STALE_MS = 3 * 60 * 1000;
1319
- const idleThresholdMs = Math.max((entry.timeoutSeconds || 600) * 1000, 10 * 60 * 1000);
1320
- // hardCeilingMs must be >= idleThresholdMs to avoid the ceiling undercutting the
1321
- // idle floor (e.g. timeoutSeconds=300 -> ceiling=7.5 min < idle=10 min would force
1322
- // zombie-guard threshold for sessions that should still use idleThresholdMs).
1323
- const hardCeilingMs = Math.max((entry.timeoutSeconds || 600) * 1000 * 1.5, idleThresholdMs * 1.5);
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 ? 2 * 60 * 1000 : idleThresholdMs;
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 ? 2 * 60 * 1000 : idleThresholdMs;
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 PING_STALE_MS_SYNC = 3 * 60 * 1000;
1620
- const idleThresholdMsSync = Math.max((entry.timeoutSeconds || 600) * 1000, 10 * 60 * 1000);
1621
- // hardCeilingMsSync must be >= idleThresholdMsSync (mirrors cmdStatus fix).
1622
- const hardCeilingMsSync = Math.max((entry.timeoutSeconds || 600) * 1000 * 1.5, idleThresholdMsSync * 1.5);
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 ? 2 * 60 * 1000 : idleThresholdMsSync;
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
+ }
@@ -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 = process.env.DISPATCH_LABELS_PATH || join(__dirname, 'labels.json');
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
- // How long a session must be idle before we proactively check result
992
- const IDLE_RESULT_CHECK_MS = 60000;
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
- deliverResult(label, result?.lastReply || terminalJsonlReply, 'completed (stop_reason=end_turn)', result?.completion || null);
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
- if (ageMs != null && ageMs >= IDLE_RESULT_CHECK_MS) {
1123
+ const idleResultCheckMs = getCurrentLivenessPolicy().idleProbeMs;
1124
+ if (ageMs != null && ageMs >= idleResultCheckMs) {
1113
1125
  const result = dispatch('result', ['--label', label]);
1114
- if (result?.lastReply || hasCompletionSignal(result?.completion)) {
1126
+ if (hasStructuredCompletion(result)) {
1115
1127
  deliverResult(label, result?.lastReply || null, null, result?.completion || null);
1116
1128
  }
1117
1129
 
1118
- const stallReason = getRunningSessionStallReason(status, IDLE_RESULT_CHECK_MS);
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
- deliverResult(label, result?.lastReply || terminalJsonlReply, 'completed (stop_reason=end_turn)', result?.completion || null);
1481
- // deliverResult exits
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
- if (ageMs != null && ageMs >= IDLE_RESULT_CHECK_MS) {
1507
+ const idleResultCheckMs = getCurrentLivenessPolicy().idleProbeMs;
1508
+ if (ageMs != null && ageMs >= idleResultCheckMs) {
1493
1509
  const result = dispatch('result', ['--label', label]);
1494
- if (result?.lastReply || hasCompletionSignal(result?.completion)) {
1510
+ if (hasStructuredCompletion(result)) {
1495
1511
  deliverResult(label, result?.lastReply || null, null, result?.completion || null);
1496
1512
  }
1497
1513
 
1498
- const stallReason = getRunningSessionStallReason(status, IDLE_RESULT_CHECK_MS);
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 (r?.lastReply || hasCompletionSignal(r?.completion)) {
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 (r2?.lastReply || hasCompletionSignal(r2?.completion)) {
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 (rExt2?.lastReply || hasCompletionSignal(rExt2?.completion)) {
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 (r3?.lastReply || hasCompletionSignal(r3?.completion)) {
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 (r4?.lastReply || hasCompletionSignal(r4?.completion)) {
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.6",
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 = process.env.DISPATCH_LABELS_PATH || join(__dirname, '..', 'dispatch', 'labels.json');
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('\\');