openclaw-scheduler 0.2.0 → 0.2.2
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 +21 -0
- package/BEST-PRACTICES.md +20 -3
- package/CHANGELOG.md +38 -1
- package/INSTALL.md +23 -0
- package/README.md +24 -12
- package/UPGRADING.md +31 -0
- package/dispatch/index.mjs +73 -28
- package/dispatch/watcher.mjs +101 -38
- package/dispatcher-delivery.js +11 -4
- package/dispatcher-strategies.js +45 -4
- package/dispatcher.js +19 -4
- package/gateway.js +117 -1
- package/jobs.js +30 -1
- package/package.json +2 -1
- package/provider-registry.js +94 -0
- package/scheduler-schema.js +5 -3
- package/schema.sql +2 -2
- package/scripts/inbox-consumer.mjs +29 -2
- package/shell-result.js +21 -3
package/AGENTS.md
CHANGED
|
@@ -27,6 +27,27 @@ For manifest authoring, validation, and identity/authorization profiles, use
|
|
|
27
27
|
- Shell jobs (`session_target: "shell"`) run without the gateway. Agent jobs
|
|
28
28
|
(`session_target: "isolated"` or `"main"`) require a running gateway.
|
|
29
29
|
|
|
30
|
+
## Checking Job Status — Always Poll, Never Infer
|
|
31
|
+
|
|
32
|
+
When reporting on whether a dispatched job is running, done, or stuck, **always call the status command directly** — never infer from check-in messages or notifications that appeared in your conversation.
|
|
33
|
+
|
|
34
|
+
Check-in messages are delivered asynchronously. By the time they appear, the job may already be finished, failed, or on a later step. Conversation messages are stale by definition.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# For chilisaus-dispatched jobs:
|
|
38
|
+
node ~/.openclaw/chilisaus/index.mjs status --label <label>
|
|
39
|
+
|
|
40
|
+
# For scheduler jobs:
|
|
41
|
+
openclaw-scheduler runs list <job-id> --json
|
|
42
|
+
openclaw-scheduler runs running --json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The `status` output gives authoritative `status` (`accepted` / `running` / `done` / `error`), `updatedAt` timestamp, and final `summary`. Use that.
|
|
46
|
+
|
|
47
|
+
**Rule: if you haven't polled status, you don't know the status.**
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
30
51
|
## Error Handling
|
|
31
52
|
|
|
32
53
|
CLI errors exit non-zero. In plain-text mode, the message goes to stderr. With
|
package/BEST-PRACTICES.md
CHANGED
|
@@ -134,7 +134,7 @@ Use `isolated` when:
|
|
|
134
134
|
|
|
135
135
|
| Rule | Bad | Good |
|
|
136
136
|
|------|-----|------|
|
|
137
|
-
| Be imperative and specific | "check kubernetes" | "Check k8s pods in
|
|
137
|
+
| Be imperative and specific | "check kubernetes" | "Check k8s pods in myapp-prod and myapp-staging. List any non-Running pods." |
|
|
138
138
|
| Include a success signal | *(nothing)* | "If all pods Running, reply with exactly: HEARTBEAT_OK" |
|
|
139
139
|
| Specify output format | *(nothing)* | "Format issues as: ⚠️ \<namespace\>/\<pod\>: \<status\>" |
|
|
140
140
|
| State available resources | *(implicit)* | "Your memory files are in ~/.openclaw/workspace/memory/" |
|
|
@@ -147,7 +147,7 @@ Check everything and let me know if anything is wrong
|
|
|
147
147
|
|
|
148
148
|
**Good prompt:**
|
|
149
149
|
```
|
|
150
|
-
Check k8s pod health across
|
|
150
|
+
Check k8s pod health across myapp-prod and myapp-staging namespaces.
|
|
151
151
|
List any non-Running pods. If all pods are Running, reply with exactly: HEARTBEAT_OK
|
|
152
152
|
Format any issues as: ⚠️ <namespace>/<pod>: <status>
|
|
153
153
|
```
|
|
@@ -282,7 +282,7 @@ When the dispatcher fires an isolated job, the agent receives a message structur
|
|
|
282
282
|
From: scheduler | result | Previous backup: 3 files committed, pushed to origin
|
|
283
283
|
---
|
|
284
284
|
|
|
285
|
-
Check k8s pod health across
|
|
285
|
+
Check k8s pod health across myapp-prod and myapp-staging.
|
|
286
286
|
If all pods are Running, reply with exactly: HEARTBEAT_OK
|
|
287
287
|
Format any issues as: ⚠️ <namespace>/<pod>: <status>
|
|
288
288
|
```
|
|
@@ -360,6 +360,23 @@ The dispatcher picks this up on its next tick (within 15s) and creates and immed
|
|
|
360
360
|
|
|
361
361
|
---
|
|
362
362
|
|
|
363
|
+
### Checking Job Status — Always Poll, Never Infer
|
|
364
|
+
|
|
365
|
+
When reporting on whether a dispatched job is running, done, or stuck, **always call `status` directly** — never infer from check-in messages that appeared in the conversation.
|
|
366
|
+
|
|
367
|
+
Check-in messages (from `agent-checkin.mjs` or similar) are delivered asynchronously via the scheduler inbox. By the time they appear in your conversation, the job may have already finished, failed, or moved on to a later step. Treating them as real-time signals produces stale or incorrect status reports.
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
# Always do this before reporting job status:
|
|
371
|
+
node ~/.openclaw/chilisaus/index.mjs status --label <label>
|
|
372
|
+
```
|
|
373
|
+
|
|
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.
|
|
375
|
+
|
|
376
|
+
**Rule: if you haven't polled `status`, you don't know the status.**
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
363
380
|
### Communicating Between Jobs
|
|
364
381
|
|
|
365
382
|
Jobs can pass data to each other using the inter-agent message queue. Messages injected into a job's context appear in the `--- Pending Messages ---` block at the top of the prompt.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,21 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.2.2] -- 2026-04-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- fix(dispatch): make completion delivery deterministic
|
|
9
|
+
- fix(dispatch): suppress junk completion fallbacks
|
|
10
|
+
- fix(package): include provider registry in npm tarball
|
|
11
|
+
- fix(scheduler): canonicalize isolated session auth overrides
|
|
12
|
+
- fix(dispatch): delete watchdog jobs on disarm instead of disable to prevent accumulation
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- feat(watcher): add stop_reason-based early delivery (Path 2a)
|
|
16
|
+
- feat(dispatch): auto-inject ORIGIN_CHAT_ID from deliverTo into prompt
|
|
17
|
+
- fix(dispatch): prefer group sessions over DM in auto-detected origin
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- ci: upgrade checkout and setup-node actions to v5
|
|
21
|
+
- docs: align packaged runtime path with host layout
|
|
22
|
+
- docs: document local npm pack install and upgrade flow
|
|
23
|
+
- test: remove dead locals in watcher coverage
|
|
24
|
+
|
|
25
|
+
## [0.2.1] -- 2026-04-01
|
|
6
26
|
|
|
7
27
|
### Fixed
|
|
8
28
|
- fix(watcher): exit cleanly when session status=done (PR #1)
|
|
9
29
|
- fix(watchdog): prevent auto-resolving active sessions with heartbeat + hard ceiling (PR #2)
|
|
10
30
|
- fix(gateway): reset idle timer while fetch is in flight (PR #3)
|
|
11
31
|
- fix(watcher): prevent premature kill of active subagent sessions with JSONL activity signal (PR #7)
|
|
32
|
+
- fix(db): add SQLite busy_timeout (5s) to prevent SQLITE_BUSY on CLI + dispatcher contention
|
|
33
|
+
- fix(approvals): prevent double-dispatch race on auto-approved jobs
|
|
34
|
+
- fix(watcher): cap deadline extension at min(timeout, 4h) to prevent zombie watchers
|
|
35
|
+
- fix(runs): preserve empty string summary/error_message (use ?? instead of ||)
|
|
36
|
+
- fix(runs): guard getTimedOutRuns against NULL run_timeout_ms on legacy rows
|
|
37
|
+
- fix(gateway): use byte length for Telegram message chunking (4096-byte limit)
|
|
38
|
+
- fix(jobs): validate schedule_tz as real IANA timezone via Intl.DateTimeFormat
|
|
39
|
+
- fix(dispatcher): wrap delete_after_run cleanup in transaction
|
|
40
|
+
- fix(dispatch): remove 4000-char truncation in formatMessageForDelivery
|
|
41
|
+
- fix(dispatch): add retry exception path delivery announcement
|
|
42
|
+
- fix(dispatch): fix dispatch CLI subcommand routing in bin wrapper
|
|
12
43
|
|
|
13
44
|
### Added
|
|
14
45
|
- feat: v0.2 runtime with identity/trust/authorization/evidence/credential handoff (PR #4)
|
|
15
46
|
- feat: x-openclaw-env-inject header for agent task credentials (PR #5)
|
|
47
|
+
- feat: [IMAGE:path] marker protocol for shell job image attachments
|
|
48
|
+
- feat: auto-delete watcher and watchdog jobs after completion (delete_after_run)
|
|
49
|
+
- feat: enforce delivery_to as required field on job INSERT
|
|
50
|
+
- feat: multi-platform CI (Linux, macOS, Windows)
|
|
16
51
|
- docs: trust architecture, multi-agent gateway routing, agent adoption files
|
|
52
|
+
- docs: AGENTS.md, CONTEXT.md, JOB-QUICK-REF.md for agent adoption
|
|
17
53
|
|
|
18
54
|
### Changed
|
|
19
55
|
- chore: replace non-ASCII characters with ASCII equivalents (PR #6)
|
|
56
|
+
- chore: bump output_excerpt_limit and output_summary_limit defaults to 64KB
|
|
20
57
|
|
|
21
58
|
## [0.2.0] -- 2026-03-11
|
|
22
59
|
|
package/INSTALL.md
CHANGED
|
@@ -39,6 +39,21 @@ npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- help
|
|
|
39
39
|
|
|
40
40
|
Runtime state for npm installs defaults to `~/.openclaw/scheduler/`, not the package directory under `node_modules/`.
|
|
41
41
|
|
|
42
|
+
To test the published package layout from local source before publishing, build a tarball and install it into a separate runtime prefix:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/amittell/openclaw-scheduler /tmp/openclaw-scheduler
|
|
46
|
+
cd /tmp/openclaw-scheduler
|
|
47
|
+
npm ci
|
|
48
|
+
npm run verify:local
|
|
49
|
+
npm pack
|
|
50
|
+
|
|
51
|
+
mkdir -p ~/.openclaw/packages/openclaw-scheduler
|
|
52
|
+
npm install --prefix ~/.openclaw/packages/openclaw-scheduler --omit=dev --no-package-lock ./openclaw-scheduler-*.tgz
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
In that setup, run the service from `~/.openclaw/packages/openclaw-scheduler/node_modules/openclaw-scheduler/dispatcher.js` and keep mutable state in `~/.openclaw/scheduler` via `SCHEDULER_HOME` and `SCHEDULER_DB`.
|
|
56
|
+
|
|
42
57
|
---
|
|
43
58
|
|
|
44
59
|
## Step 2: Install Dependencies
|
|
@@ -223,6 +238,14 @@ npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- setup --service-mo
|
|
|
223
238
|
npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- setup --service-mode daemon
|
|
224
239
|
```
|
|
225
240
|
|
|
241
|
+
If you installed from a locally packed tarball:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
npm exec --prefix ~/.openclaw/packages/openclaw-scheduler openclaw-scheduler -- setup --service-mode agent
|
|
245
|
+
# or:
|
|
246
|
+
npm exec --prefix ~/.openclaw/packages/openclaw-scheduler openclaw-scheduler -- setup --service-mode daemon
|
|
247
|
+
```
|
|
248
|
+
|
|
226
249
|
What each mode does:
|
|
227
250
|
|
|
228
251
|
- `agent` writes `~/Library/LaunchAgents/ai.openclaw.scheduler.plist` and bootstraps `gui/$UID/ai.openclaw.scheduler`
|
package/README.md
CHANGED
|
@@ -206,6 +206,23 @@ npm run verify:smoke # lightweight smoke gate used by GitHub Act
|
|
|
206
206
|
|
|
207
207
|
GitHub Actions runs the smoke gate plus the in-memory test suite on Linux, macOS, and Windows with Node 20. The full release gate still runs locally via `npm run verify:local` and is enforced again by `prepublishOnly`.
|
|
208
208
|
|
|
209
|
+
### Option C: local npm pack (simulate the published package from source)
|
|
210
|
+
|
|
211
|
+
Use this when you want to test the exact package layout end users get from npm without publishing to npmjs.org yet.
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
git clone https://github.com/amittell/openclaw-scheduler /tmp/openclaw-scheduler
|
|
215
|
+
cd /tmp/openclaw-scheduler
|
|
216
|
+
npm ci
|
|
217
|
+
npm run verify:local
|
|
218
|
+
npm pack
|
|
219
|
+
|
|
220
|
+
mkdir -p ~/.openclaw/packages/openclaw-scheduler
|
|
221
|
+
npm install --prefix ~/.openclaw/packages/openclaw-scheduler --omit=dev --no-package-lock ./openclaw-scheduler-*.tgz
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Point your service at `~/.openclaw/packages/openclaw-scheduler/node_modules/openclaw-scheduler/dispatcher.js`, and keep mutable state in `~/.openclaw/scheduler` via `SCHEDULER_HOME` and `SCHEDULER_DB`.
|
|
225
|
+
|
|
209
226
|
The package also exports a small safe programmatic API surface for tooling:
|
|
210
227
|
|
|
211
228
|
```js
|
|
@@ -1140,18 +1157,13 @@ Use this when you want scripts to enqueue only actionable signals, then a single
|
|
|
1140
1157
|
# 1) Enqueue a signal
|
|
1141
1158
|
openclaw-scheduler msg send monitor-agent main "Found 3 critical errors in prod logs"
|
|
1142
1159
|
|
|
1143
|
-
# 2)
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
"delivery_channel": "telegram",
|
|
1151
|
-
"delivery_to": "YOUR_CHAT_ID",
|
|
1152
|
-
"run_timeout_ms": 60000,
|
|
1153
|
-
"origin": "system"
|
|
1154
|
-
}'
|
|
1160
|
+
# 2) Run the inbox consumer daemon (event-driven, watches SQLite WAL)
|
|
1161
|
+
# Delivers within ~250ms of a message landing in the DB.
|
|
1162
|
+
# No polling cron needed — the daemon watches for WAL changes.
|
|
1163
|
+
node scripts/inbox-consumer.mjs --watch --to YOUR_CHAT_ID --channel telegram
|
|
1164
|
+
|
|
1165
|
+
# Or as a one-shot drain (useful for testing):
|
|
1166
|
+
node scripts/inbox-consumer.mjs --to YOUR_CHAT_ID --channel telegram
|
|
1155
1167
|
```
|
|
1156
1168
|
|
|
1157
1169
|
---
|
package/UPGRADING.md
CHANGED
|
@@ -105,6 +105,24 @@ npm install --prefix ~/.openclaw/scheduler openclaw-scheduler@latest
|
|
|
105
105
|
npm install --prefix $env:USERPROFILE\.openclaw\scheduler openclaw-scheduler@latest
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
### Local source tarball upgrade (`npm pack`)
|
|
109
|
+
|
|
110
|
+
Use this when you want to upgrade a host to a locally built package before publishing to npmjs.org.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git clone https://github.com/amittell/openclaw-scheduler /tmp/openclaw-scheduler
|
|
114
|
+
cd /tmp/openclaw-scheduler
|
|
115
|
+
git pull
|
|
116
|
+
npm ci
|
|
117
|
+
npm run verify:local
|
|
118
|
+
npm pack
|
|
119
|
+
|
|
120
|
+
mkdir -p ~/.openclaw/packages/openclaw-scheduler
|
|
121
|
+
npm install --prefix ~/.openclaw/packages/openclaw-scheduler --omit=dev --no-package-lock ./openclaw-scheduler-*.tgz
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Keep `SCHEDULER_HOME=~/.openclaw/scheduler` and `SCHEDULER_DB=~/.openclaw/scheduler/scheduler.db`, and point the service at `~/.openclaw/packages/openclaw-scheduler/node_modules/openclaw-scheduler/dispatcher.js`.
|
|
125
|
+
|
|
108
126
|
---
|
|
109
127
|
|
|
110
128
|
## Step 2: Install Dependencies
|
|
@@ -277,6 +295,19 @@ sleep 3
|
|
|
277
295
|
ssh $HOST "tail -5 /tmp/openclaw-scheduler.log && cd ~/.openclaw/scheduler && node cli.js status"
|
|
278
296
|
```
|
|
279
297
|
|
|
298
|
+
#### macOS host over SSH using a local tarball
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
HOST=youruser@your-mac-host.lan
|
|
302
|
+
TARBALL=./openclaw-scheduler-*.tgz
|
|
303
|
+
|
|
304
|
+
scp $TARBALL $HOST:~/.openclaw/
|
|
305
|
+
ssh $HOST "mkdir -p ~/.openclaw/packages/openclaw-scheduler && npm install --prefix ~/.openclaw/packages/openclaw-scheduler --omit=dev --no-package-lock ~/.openclaw/$(basename $TARBALL)"
|
|
306
|
+
ssh $HOST "launchctl kickstart -k gui/\$(id -u)/ai.openclaw.scheduler"
|
|
307
|
+
sleep 3
|
|
308
|
+
ssh $HOST "tail -5 /tmp/openclaw-scheduler.log && launchctl print gui/\$(id -u)/ai.openclaw.scheduler | sed -n '1,20p'"
|
|
309
|
+
```
|
|
310
|
+
|
|
280
311
|
#### Linux / Windows WSL2 host over SSH
|
|
281
312
|
|
|
282
313
|
```bash
|
package/dispatch/index.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { randomUUID } from 'crypto';
|
|
|
32
32
|
import { execFileSync } from 'child_process';
|
|
33
33
|
import { homedir } from 'os';
|
|
34
34
|
import Database from 'better-sqlite3';
|
|
35
|
+
import { buildTerminalCompletionPayload } from './completion.mjs';
|
|
35
36
|
import { onStarted, onFinished, onStuck } from './hooks.mjs';
|
|
36
37
|
|
|
37
38
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -334,14 +335,28 @@ function readSessionsStore(agent = 'main') {
|
|
|
334
335
|
*
|
|
335
336
|
* @returns {string|null} - e.g. "telegram:-100200000000", or null if not found
|
|
336
337
|
*/
|
|
338
|
+
/**
|
|
339
|
+
* Infer the chat type ("group" | "direct" | "") from a session object and its key.
|
|
340
|
+
* Checks session.chatType first, then falls back to key pattern matching.
|
|
341
|
+
* Key patterns: agent:main:<channel>:group:<id> → group
|
|
342
|
+
* agent:main:<channel>:direct:<id> → direct
|
|
343
|
+
*/
|
|
344
|
+
function inferChatType(key, session) {
|
|
345
|
+
if (session.chatType) return session.chatType;
|
|
346
|
+
if (key.includes(":group:")) return "group";
|
|
347
|
+
if (key.includes(":direct:")) return "direct";
|
|
348
|
+
return "";
|
|
349
|
+
}
|
|
350
|
+
|
|
337
351
|
function getActiveOriginFromSessions() {
|
|
338
352
|
const store = readSessionsStore("main");
|
|
339
353
|
if (!store) return null;
|
|
340
354
|
|
|
341
|
-
let best = null;
|
|
342
|
-
let bestTime = 0;
|
|
343
355
|
const TEN_MIN_MS = 10 * 60 * 1000;
|
|
344
356
|
|
|
357
|
+
/** @type {Array<{origin: string, updatedAt: number, chatType: string}>} */
|
|
358
|
+
const candidates = [];
|
|
359
|
+
|
|
345
360
|
for (const [key, session] of Object.entries(store)) {
|
|
346
361
|
// Only consider main sessions, not subagents
|
|
347
362
|
// Pattern: agent:main:<channel>:<type>:<id> but NOT agent:main:subagent:*
|
|
@@ -357,19 +372,37 @@ function getActiveOriginFromSessions() {
|
|
|
357
372
|
// Must be recently active
|
|
358
373
|
if (Date.now() - updatedAt > TEN_MIN_MS) continue;
|
|
359
374
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
375
|
+
// Prefer deliveryContext.to if available
|
|
376
|
+
const deliveryTo = session.deliveryContext?.to || null;
|
|
377
|
+
if (!deliveryTo) continue;
|
|
378
|
+
|
|
379
|
+
candidates.push({
|
|
380
|
+
origin: deliveryTo,
|
|
381
|
+
updatedAt,
|
|
382
|
+
chatType: inferChatType(key, session),
|
|
383
|
+
});
|
|
370
384
|
}
|
|
371
385
|
|
|
372
|
-
return
|
|
386
|
+
if (candidates.length === 0) return null;
|
|
387
|
+
|
|
388
|
+
// Tiebreaker: prefer group sessions over direct/DM sessions.
|
|
389
|
+
// When both a DM and a group session are recently active, the DM session
|
|
390
|
+
// often has a more recent updatedAt (agent just replied there), but the
|
|
391
|
+
// triggering context was the group chat. Within the same chat type, prefer
|
|
392
|
+
// the most recently updated session.
|
|
393
|
+
const typeScore = (chatType) => {
|
|
394
|
+
if (chatType === "group") return 2;
|
|
395
|
+
if (chatType === "direct") return 0;
|
|
396
|
+
return 1; // unknown / other
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
candidates.sort((a, b) => {
|
|
400
|
+
const scoreDiff = typeScore(b.chatType) - typeScore(a.chatType);
|
|
401
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
402
|
+
return b.updatedAt - a.updatedAt;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return candidates[0].origin;
|
|
373
406
|
}
|
|
374
407
|
|
|
375
408
|
/**
|
|
@@ -507,12 +540,12 @@ function disarmWatchdog(label) {
|
|
|
507
540
|
if (!entry?.watchdogJobId) return;
|
|
508
541
|
try {
|
|
509
542
|
const schedulerCli = join(__dirname, '..', 'cli.js');
|
|
510
|
-
execFileSync(process.execPath, [schedulerCli, 'jobs', '
|
|
543
|
+
execFileSync(process.execPath, [schedulerCli, 'jobs', 'delete', entry.watchdogJobId], {
|
|
511
544
|
encoding: 'utf-8',
|
|
512
545
|
timeout: 5000,
|
|
513
546
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
514
547
|
});
|
|
515
|
-
process.stderr.write(`[${BRAND}] watchdog
|
|
548
|
+
process.stderr.write(`[${BRAND}] watchdog deleted for ${label}\n`);
|
|
516
549
|
} catch (err) {
|
|
517
550
|
process.stderr.write(`[${BRAND}] watchdog disarm failed for ${label}: ${err.message}\n`);
|
|
518
551
|
}
|
|
@@ -599,6 +632,14 @@ async function cmdEnqueue(flags) {
|
|
|
599
632
|
const deliverMode = flags['delivery-mode'] || 'announce';
|
|
600
633
|
const mode = flags.mode || 'fresh';
|
|
601
634
|
|
|
635
|
+
// -- Auto-inject ORIGIN_CHAT_ID into prompt message ---------
|
|
636
|
+
// Ensures the spawned agent always knows where to send message tool calls,
|
|
637
|
+
// matching the delivery target. Skip if already present (caller is explicit)
|
|
638
|
+
// or if there's no delivery target.
|
|
639
|
+
if (deliverTo && !message.includes('ORIGIN_CHAT_ID:')) {
|
|
640
|
+
message = `ORIGIN_CHAT_ID: ${deliverTo}\n\n${message}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
602
643
|
// -- Verify command flag -----------------------------------
|
|
603
644
|
const verifyCmd = flags['verify-cmd'] || null;
|
|
604
645
|
|
|
@@ -849,7 +890,8 @@ async function cmdEnqueue(flags) {
|
|
|
849
890
|
const watcherPath = join(__dirname, 'watcher.mjs');
|
|
850
891
|
// Watcher timeout = session timeout + 120s buffer for startup/polling
|
|
851
892
|
const watcherTimeoutS = timeoutS + 120;
|
|
852
|
-
const
|
|
893
|
+
const idleThresholdS = flags['idle-threshold'] || '300';
|
|
894
|
+
const watcherCmd = `DISPATCH_LABELS_PATH='${sq(LABELS_PATH)}' '${sq(process.execPath)}' '${sq(watcherPath)}' --label '${sq(label)}' --timeout ${watcherTimeoutS} --poll-interval 20 --idle-threshold ${idleThresholdS}`;
|
|
853
895
|
|
|
854
896
|
const nowUtc = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
855
897
|
const jobSpec = JSON.stringify({
|
|
@@ -1116,6 +1158,7 @@ function cmdStatus(flags) {
|
|
|
1116
1158
|
spawnedAt: current.spawnedAt,
|
|
1117
1159
|
updatedAt: current.updatedAt,
|
|
1118
1160
|
summary: current.summary || null,
|
|
1161
|
+
completion: current.completion || null,
|
|
1119
1162
|
error: current.error || null,
|
|
1120
1163
|
liveness,
|
|
1121
1164
|
...(syncAction ? { syncAction } : {}),
|
|
@@ -1405,6 +1448,7 @@ function cmdResult(flags) {
|
|
|
1405
1448
|
status: entry.status,
|
|
1406
1449
|
spawnedAt: entry.spawnedAt,
|
|
1407
1450
|
summary: entry.summary || (lastReply ? lastReply.slice(0, 500) : null),
|
|
1451
|
+
completion: entry.completion || null,
|
|
1408
1452
|
lastReply: lastReply || null,
|
|
1409
1453
|
error: entry.error || null,
|
|
1410
1454
|
});
|
|
@@ -1474,15 +1518,15 @@ async function cmdDone(flags) {
|
|
|
1474
1518
|
}
|
|
1475
1519
|
}
|
|
1476
1520
|
|
|
1477
|
-
//
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1521
|
+
// Summary passes through as-is for raw diagnostics, but we also persist a
|
|
1522
|
+
// first-class completion payload with deterministic delivery text so the
|
|
1523
|
+
// watcher/post-office path never depends solely on transcript recovery.
|
|
1524
|
+
const completion = buildTerminalCompletionPayload({
|
|
1525
|
+
summary: rawSummary,
|
|
1526
|
+
checklist,
|
|
1527
|
+
sha,
|
|
1528
|
+
});
|
|
1529
|
+
const summary = completion.summary || rawSummary;
|
|
1486
1530
|
|
|
1487
1531
|
const existing = getLabel(label);
|
|
1488
1532
|
|
|
@@ -1598,7 +1642,7 @@ async function cmdDone(flags) {
|
|
|
1598
1642
|
// Label was never registered (e.g. direct subagent spawn, not via enqueue).
|
|
1599
1643
|
// This is not an error -- the work completed, the label just wasn't tracked.
|
|
1600
1644
|
process.stderr.write(`[${BRAND}] warn: no session found for label "${label}" -- registering as done\n`);
|
|
1601
|
-
setLabel(label, { status: 'done', summary, ...(sha ? { sha } : {}) });
|
|
1645
|
+
setLabel(label, { status: 'done', summary, completion, ...(sha ? { sha } : {}) });
|
|
1602
1646
|
|
|
1603
1647
|
// No watcher is polling for this label, so actively notify via the gateway
|
|
1604
1648
|
// post office using delivery config from config.json as fallback target.
|
|
@@ -1622,13 +1666,14 @@ async function cmdDone(flags) {
|
|
|
1622
1666
|
process.stderr.write(`[${BRAND}] warn: no deliverTo in config -- completion not delivered for "${label}"\n`);
|
|
1623
1667
|
}
|
|
1624
1668
|
|
|
1625
|
-
out({ ok: true, label, status: 'done', summary, message: 'Label not previously registered; marked done.' });
|
|
1669
|
+
out({ ok: true, label, status: 'done', summary, completion, message: 'Label not previously registered; marked done.' });
|
|
1626
1670
|
return;
|
|
1627
1671
|
}
|
|
1628
1672
|
|
|
1629
1673
|
setLabel(label, {
|
|
1630
1674
|
status: 'done',
|
|
1631
1675
|
summary,
|
|
1676
|
+
completion,
|
|
1632
1677
|
...(sha ? { sha } : {}),
|
|
1633
1678
|
});
|
|
1634
1679
|
|
|
@@ -1647,7 +1692,7 @@ async function cmdDone(flags) {
|
|
|
1647
1692
|
session_key: existing.sessionKey || null,
|
|
1648
1693
|
}).catch(() => {});
|
|
1649
1694
|
|
|
1650
|
-
out({ ok: true, label, status: 'done', summary, message: 'Label marked done via agent signal.' });
|
|
1695
|
+
out({ ok: true, label, status: 'done', summary, completion, message: 'Label marked done via agent signal.' });
|
|
1651
1696
|
}
|
|
1652
1697
|
|
|
1653
1698
|
/**
|