@luckydraw/cumulus 1.0.4 → 1.0.5
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/CHANGELOG.md +10 -0
- package/dist/gateway/daemon.d.ts.map +1 -1
- package/dist/gateway/daemon.js +53 -18
- package/dist/gateway/daemon.js.map +1 -1
- package/dist/gateway/gateway-agents-mcp.js +133 -0
- package/dist/gateway/gateway-agents-mcp.js.map +1 -1
- package/dist/gateway/jobs.d.ts +158 -0
- package/dist/gateway/jobs.d.ts.map +1 -0
- package/dist/gateway/jobs.js +497 -0
- package/dist/gateway/jobs.js.map +1 -0
- package/dist/gateway/scheduler.d.ts.map +1 -1
- package/dist/gateway/scheduler.js +9 -30
- package/dist/gateway/scheduler.js.map +1 -1
- package/dist/gateway/server.d.ts +16 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +183 -2
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/setup.d.ts.map +1 -1
- package/dist/gateway/setup.js +10 -0
- package/dist/gateway/setup.js.map +1 -1
- package/dist/lib/gateway.d.ts +11 -42
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +33 -96
- package/dist/lib/gateway.js.map +1 -1
- package/dist/lib/tool-inventory.d.ts.map +1 -1
- package/dist/lib/tool-inventory.js +9 -1
- package/dist/lib/tool-inventory.js.map +1 -1
- package/docs/conditional-continuation.md +102 -147
- package/package.json +1 -1
|
@@ -1,167 +1,122 @@
|
|
|
1
|
-
# Conditional Thread Continuation —
|
|
1
|
+
# Conditional Thread Continuation — `run_job`
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
**Status:** Gateway-native since task 139 (2026-08-15). Supersedes the
|
|
4
|
+
watcher-with-deadline convention adopted 2026-07-26, which is described at the
|
|
5
|
+
end for anyone reading old threads.
|
|
6
6
|
|
|
7
7
|
## The problem
|
|
8
8
|
|
|
9
|
-
A thread kicks off something long-running with an uncertain finish time — a
|
|
10
|
-
test suite, a deploy, a download. The thread should continue **the
|
|
11
|
-
known** (success _or_ failure), not after an arbitrary
|
|
12
|
-
scheduled AI turn every N minutes to poll ("did it
|
|
13
|
-
history — the exact failure class tasks
|
|
9
|
+
A thread kicks off something long-running with an uncertain finish time — a
|
|
10
|
+
compile, a test suite, a deploy, a download. The thread should continue **the
|
|
11
|
+
moment the outcome is known** (success _or_ failure), not after an arbitrary
|
|
12
|
+
sleep, and not by burning a scheduled AI turn every N minutes to poll ("did it
|
|
13
|
+
finish yet?" cron turns dilute thread history — the exact failure class tasks
|
|
14
|
+
088–092 fixed).
|
|
14
15
|
|
|
15
|
-
## The
|
|
16
|
+
## The answer
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
```
|
|
19
|
+
run_job("npm run build", label: "build")
|
|
20
|
+
→ { started: true, id: "job_a7f3c2d1", logPath: "..." }
|
|
21
|
+
```
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`server.ts handleAgentInject`):
|
|
23
|
+
Then **end the turn**. When the job exits, the gateway starts a new turn on the
|
|
24
|
+
thread carrying the exit code, the working directory, the log path and the tail
|
|
25
|
+
of the output.
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
|
|
27
|
-
-d '{"targets": "THREAD_NAME", "sender": "build-watcher", "message": "..."}'
|
|
28
|
-
```
|
|
27
|
+
Companions: `list_jobs()`, `job_log(id, tail?)` (works while it is still
|
|
28
|
+
running, so progress can be checked without waiting), `cancel_job(id)`.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
`target`/`from` — those names will 400.
|
|
32
|
-
- Auth: `X-API-Key: <key>` or `Authorization: Bearer <key>`.
|
|
33
|
-
- On thundercat, a key is readable from the gateway config:
|
|
34
|
-
`KEY=$(jq -r '.apiKeys[0]' "${CUMULUS_DIR:-$HOME/.cumulus}/gateway.config.json")`
|
|
35
|
-
Read it through `$CUMULUS_DIR`, never as a literal `~/.cumulus` — a hard-coded path
|
|
36
|
-
points a sandboxed run (tests, a second gateway) straight at the production gateway
|
|
37
|
-
with the production admin key (task 111).
|
|
38
|
-
|
|
39
|
-
### 2. `schedule_trigger` MCP tool — the time-based floor
|
|
40
|
-
|
|
41
|
-
Available in every thread (gateway-agents MCP). `{id, trigger: "once", at: "<ISO datetime>",
|
|
42
|
-
message}` fires one injection at a fixed time then auto-removes; `trigger: "cron"` +
|
|
43
|
-
`cron` recurs (1-minute resolution). Persisted in thread config — **survives gateway
|
|
44
|
-
restarts**, which the watcher does not. Companions: `list_schedules`, `cancel_schedule`.
|
|
45
|
-
|
|
46
|
-
## The pattern
|
|
47
|
-
|
|
48
|
-
**Watcher for immediacy, one-shot schedule for the guarantee.** The condition-watcher
|
|
49
|
-
doesn't need to live in the gateway: the thread's Claude subprocess runs on the same
|
|
50
|
-
machine and can leave a detached process behind.
|
|
51
|
-
|
|
52
|
-
### Step 1 — detach the job with a watcher wrapper
|
|
53
|
-
|
|
54
|
-
For a _process-exit_ condition (the common case — build/test/deploy), the wrapper simply
|
|
55
|
-
runs the job and fires when it exits, success or error:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
KEY=$(jq -r '.apiKeys[0]' "${CUMULUS_DIR:-$HOME/.cumulus}/gateway.config.json")
|
|
59
|
-
nohup sh -c '
|
|
60
|
-
npm run build > /tmp/build.log 2>&1
|
|
61
|
-
rc=$?
|
|
62
|
-
curl -s -X POST http://localhost:8090/api/agents/inject \
|
|
63
|
-
-H "X-API-Key: '"$KEY"'" -H "Content-Type: application/json" \
|
|
64
|
-
-d "{\"targets\":\"cumulus\",\"sender\":\"build-watcher\",
|
|
65
|
-
\"message\":\"[watcher] Build finished, exit $rc. Log: /tmp/build.log. First: cancel_schedule(\\\"build-deadline\\\").\"}"
|
|
66
|
-
' >/dev/null 2>&1 &
|
|
67
|
-
```
|
|
30
|
+
### Write the command plainly
|
|
68
31
|
|
|
69
|
-
|
|
70
|
-
|
|
32
|
+
No `&`, no `> log 2>&1`, no `nohup`, no `setsid`. The gateway supplies all of
|
|
33
|
+
that, and adding your own breaks the log capture that the completion report
|
|
34
|
+
reads from. A command ending in `exit N` is fine — the status is captured by an
|
|
35
|
+
`EXIT` trap, not by trailing statements.
|
|
71
36
|
|
|
72
|
-
|
|
73
|
-
until <check-command>; do sleep 5; done # e.g. curl -sf http://localhost:3000/health
|
|
74
|
-
```
|
|
37
|
+
### For a condition that is not a process exit
|
|
75
38
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- **`>/dev/null 2>&1 &`** — redirecting _both_ streams is what makes the watcher survive
|
|
79
|
-
the Claude turn ending, and it is the only part that does. The turn's subprocess exits
|
|
80
|
-
at turn end; anything still holding its inherited stdout/stderr pipes is killed by
|
|
81
|
-
SIGPIPE (exit 141) on the next write. `nohup` alone does not save it (it ignores SIGHUP,
|
|
82
|
-
the wrong signal, and only auto-redirects when stdout is a terminal — here it is a pipe),
|
|
83
|
-
and neither does `setsid` (nothing is signalling the process group). Prefer a real log
|
|
84
|
-
file over `/dev/null` when the woken turn will need to diagnose. Surviving the turn is
|
|
85
|
-
all this buys — the watcher is still unsupervised and does NOT survive a machine reboot;
|
|
86
|
-
that's what step 2 is for.
|
|
87
|
-
- **Always report failure too.** The wrapper fires on _outcome_, not on _success_ —
|
|
88
|
-
include the exit code and a log path so the woken turn can diagnose without re-running.
|
|
89
|
-
- **Tell the woken turn to cancel the deadline** (put it in the message, as above) so the
|
|
90
|
-
fallback never double-fires.
|
|
91
|
-
- Name the `sender` after the condition (`build-watcher`, `deploy-watcher`) — it prefixes
|
|
92
|
-
the injected message and reads clearly in history.
|
|
93
|
-
- **Do not reply to the watcher.** Injected messages carry a "Reply using
|
|
94
|
-
`send_to_agent(...)`" footer intended for agent-to-agent traffic. A watcher is a script,
|
|
95
|
-
not an agent, so replying to it mints an empty thread named after the sender and burns a
|
|
96
|
-
turn in it. Measured while verifying task 109: a woken turn replied to `job-watcher` and
|
|
97
|
-
a `job-watcher` thread appeared on disk. Act on the report instead.
|
|
98
|
-
|
|
99
|
-
### Step 2 — set the deadline fallback
|
|
100
|
-
|
|
101
|
-
In the same turn that spawns the watcher, set a one-shot schedule as the floor:
|
|
39
|
+
Put the wait inside the job. A job _is_ a shell, so the same loop that used to
|
|
40
|
+
go in a watcher goes here, and it inherits supervision and reporting for free:
|
|
102
41
|
|
|
103
42
|
```
|
|
104
|
-
|
|
105
|
-
id: "build-deadline",
|
|
106
|
-
trigger: "once",
|
|
107
|
-
at: "<now + generous timeout, ISO 8601>",
|
|
108
|
-
message: "[deadline] The build watcher never reported back. Check /tmp/build.log and whether the process is still running."
|
|
109
|
-
})
|
|
43
|
+
run_job("until curl -sf http://localhost:3000/health; do sleep 5; done", label: "wait for health")
|
|
110
44
|
```
|
|
111
45
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
46
|
+
### When NOT to use it
|
|
47
|
+
|
|
48
|
+
A command that finishes within a couple of minutes should just run in the
|
|
49
|
+
foreground. If it runs past the Bash tool's limit the harness backgrounds it and
|
|
50
|
+
notifies the thread, which keeps waiting in-turn — correct for twenty minutes,
|
|
51
|
+
wrong for six hours. `run_job` is for the second case, and for when the thread
|
|
52
|
+
should be free meanwhile.
|
|
53
|
+
|
|
54
|
+
## Why this is a tool and not a documented recipe
|
|
55
|
+
|
|
56
|
+
Cumulus tried the documented-recipe route three times — task 108 (redirect both
|
|
57
|
+
streams), 109 (watcher + deadline), 116 (the worked one-liner in the content
|
|
58
|
+
store) — and it kept failing in the world, at a standing cost of ~290 prompt
|
|
59
|
+
tokens on every turn of every thread. Three measured reasons:
|
|
60
|
+
|
|
61
|
+
1. **The cure had to be applied at every layer.** A driver script that redirects
|
|
62
|
+
its own output does not save the children it spawns. @ordimor got two of
|
|
63
|
+
three layers right and lost the job anyway.
|
|
64
|
+
2. **The watcher could die independently of the job.** It was itself background
|
|
65
|
+
work with its own redirect requirement. Task 109's postmortem found exactly
|
|
66
|
+
that: the watcher never fired.
|
|
67
|
+
3. **One killer was not curable from the thread side at all.** systemd's default
|
|
68
|
+
`KillMode=control-group` reaps every process in the unit's cgroup when the
|
|
69
|
+
main process exits, and `systemctl reload` makes the gateway exit (Rule #9).
|
|
70
|
+
Neither `setsid`, `nohup`, nor redirecting escapes a cgroup — a cgroup is not
|
|
71
|
+
a session. Every reload killed every background job on the box.
|
|
72
|
+
|
|
73
|
+
`run_job` removes 1 and 2 by construction: the daemon spawns the job with a file
|
|
74
|
+
descriptor rather than a pipe (so SIGPIPE is unreachable, not cured) and outside
|
|
75
|
+
the turn's process tree (so interjecting cannot reach it), and the registry that
|
|
76
|
+
reports completion cannot die independently of the daemon that owns the job.
|
|
77
|
+
|
|
78
|
+
Killer 3 needs `KillMode=process` on the service unit — shipped in the unit
|
|
79
|
+
template generated by `cumulus-gateway setup`. **Without it, jobs still work but
|
|
80
|
+
do not survive a gateway restart.** They are not lost silently either way: on
|
|
81
|
+
startup the registry checks every recorded job, re-adopts the ones still
|
|
82
|
+
running, and reports the rest — with their real exit code if the job got far
|
|
83
|
+
enough to record one, otherwise honestly as `interrupted`, which explicitly says
|
|
84
|
+
the work may or may not have completed.
|
|
85
|
+
|
|
86
|
+
## `schedule_trigger` is still here, for a different job
|
|
87
|
+
|
|
88
|
+
`schedule_trigger({id, trigger: "once"|"cron", at|cron, message})` injects a
|
|
89
|
+
message into the thread at a **time**. That is deferred reminders, drip
|
|
90
|
+
sequences and genuinely periodic work — not condition-waiting.
|
|
91
|
+
|
|
92
|
+
It is no longer needed as a deadline backstop for background work: that leg
|
|
93
|
+
existed because an unsupervised watcher could vanish, and the registry cannot.
|
|
116
94
|
|
|
117
|
-
###
|
|
95
|
+
### Anti-pattern: cron polling turns
|
|
118
96
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
97
|
+
Do **not** use `trigger: "cron"` to poll a condition every N minutes. Each poll
|
|
98
|
+
burns a full AI turn and appends "checked, nothing yet" noise to thread history,
|
|
99
|
+
degrading retrieval for every future turn.
|
|
122
100
|
|
|
123
|
-
|
|
101
|
+
## Superseded: the watcher-with-deadline convention (2026-07-26 → 2026-08-15)
|
|
102
|
+
|
|
103
|
+
Threads used to compose two primitives by hand: a detached `curl` to
|
|
104
|
+
`POST /api/agents/inject` wrapped around the job, plus a one-shot
|
|
105
|
+
`schedule_trigger` as the floor in case the watcher died. Recorded here only so
|
|
106
|
+
older thread history reads coherently — do not write new ones.
|
|
107
|
+
|
|
108
|
+
Its four known limitations are what `run_job` was built to remove:
|
|
109
|
+
|
|
110
|
+
| Limitation of the convention | Status |
|
|
111
|
+
| ------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
112
|
+
| No supervision — a bare background process, nothing restarts or tracks it | Fixed: the registry owns the job |
|
|
113
|
+
| No visibility — no way to enumerate or cancel a pending watcher | Fixed: `list_jobs` / `job_log` / `cancel_job` |
|
|
114
|
+
| Needed a gateway API key in the script's command line | Fixed: no key involved at all |
|
|
115
|
+
| Reboot amnesia — schedules survived restarts, watchers did not | Fixed: adoption on startup, or an honest `interrupted` report |
|
|
124
116
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
thread
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
These are accepted trade-offs of the convention, and the reason a robust version may be
|
|
133
|
-
worth building later:
|
|
134
|
-
|
|
135
|
-
1. **No supervision** — the watcher is a bare background process; nothing restarts it.
|
|
136
|
-
The deadline fallback bounds the damage but adds latency in the failure case.
|
|
137
|
-
2. **No visibility** — there is no `list_watches`; a pending watcher is invisible to the
|
|
138
|
-
thread, the user, and the dashboard. You can't enumerate or cancel what you can't see.
|
|
139
|
-
3. **Key in the script** — the watcher needs a gateway API key in its environment/command
|
|
140
|
-
line. On thundercat (single-user box) this is acceptable; it would not be under
|
|
141
|
-
namespace-scoped multi-tenant use (task 097 P7).
|
|
142
|
-
4. **Reboot amnesia** — schedules survive restarts, watchers don't. After a reboot only
|
|
143
|
-
the deadline leg remains.
|
|
144
|
-
|
|
145
|
-
## Later: the robust version (`await_condition`, gateway-native)
|
|
146
|
-
|
|
147
|
-
If condition-waiting becomes a recurring pattern, the honest fix is a first-class
|
|
148
|
-
gateway watcher living beside `scheduler.ts` (per Rule #2 it needs a task doc before any
|
|
149
|
-
implementation). Sketch:
|
|
150
|
-
|
|
151
|
-
- **Tool:** `await_condition({id, check, interval, timeout, message})` — the gateway
|
|
152
|
-
polls `check` (a shell predicate) every `interval` seconds and injects `message` the
|
|
153
|
-
moment it exits 0. `timeout` fires a "condition never came true" injection instead of
|
|
154
|
-
waiting forever.
|
|
155
|
-
- **Persistence:** watches stored in thread config like schedules → survive gateway
|
|
156
|
-
restarts and machine reboots (poll loop resumes on startup). This alone removes
|
|
157
|
-
limitations 1, 2, and 4.
|
|
158
|
-
- **Visibility:** `list_watches` / `cancel_watch` companions, same shape as
|
|
159
|
-
`list_schedules` / `cancel_schedule`; dashboard can surface pending watches.
|
|
160
|
-
- **Reuse:** fires through the same `sendMessage` path as the scheduler, so queueing,
|
|
161
|
-
busy→idle drain, and history semantics are identical to today's injections.
|
|
162
|
-
- **Design question to settle in the task doc:** the predicate is the gateway executing a
|
|
163
|
-
configured shell command. Fine for admin threads on a single-user box; under P7
|
|
164
|
-
namespace scoping it needs an allowlist or per-namespace opt-in before scoped keys can
|
|
165
|
-
create watches.
|
|
166
|
-
|
|
167
|
-
Until then: watcher + deadline, as above.
|
|
117
|
+
The key point was also a real hazard rather than a theoretical one: the recipe
|
|
118
|
+
read `apiKeys[0]` — the gateway's **admin** key — and because it was seeded into
|
|
119
|
+
every thread's content store as topic-free boilerplate, it surfaced on
|
|
120
|
+
unrelated queries and taught an app's visitor-facing model to invent HTTP calls
|
|
121
|
+
against the gateway (task 134). `run_job` needs no credential, and the seeded
|
|
122
|
+
recipe has been deleted.
|