@luckydraw/cumulus 0.31.66 → 1.0.0

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +6 -559
  2. package/LICENSE +150 -0
  3. package/README.md +27 -8
  4. package/dist/gateway/adapters/webchat.d.ts +2 -0
  5. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  6. package/dist/gateway/adapters/webchat.js +22 -2
  7. package/dist/gateway/adapters/webchat.js.map +1 -1
  8. package/dist/gateway/config.d.ts +17 -2
  9. package/dist/gateway/config.d.ts.map +1 -1
  10. package/dist/gateway/config.js +10 -3
  11. package/dist/gateway/config.js.map +1 -1
  12. package/dist/gateway/daemon.d.ts +3 -1
  13. package/dist/gateway/daemon.d.ts.map +1 -1
  14. package/dist/gateway/daemon.js +128 -39
  15. package/dist/gateway/daemon.js.map +1 -1
  16. package/dist/gateway/namespaces.d.ts +34 -0
  17. package/dist/gateway/namespaces.d.ts.map +1 -1
  18. package/dist/gateway/namespaces.js +58 -0
  19. package/dist/gateway/namespaces.js.map +1 -1
  20. package/dist/gateway/server.d.ts +8 -0
  21. package/dist/gateway/server.d.ts.map +1 -1
  22. package/dist/gateway/server.js +150 -41
  23. package/dist/gateway/server.js.map +1 -1
  24. package/dist/gateway/setup.d.ts +32 -0
  25. package/dist/gateway/setup.d.ts.map +1 -1
  26. package/dist/gateway/setup.js +23 -3
  27. package/dist/gateway/setup.js.map +1 -1
  28. package/dist/gateway/static/widget.js +897 -611
  29. package/dist/lib/gateway.d.ts +30 -8
  30. package/dist/lib/gateway.d.ts.map +1 -1
  31. package/dist/lib/gateway.js +36 -11
  32. package/dist/lib/gateway.js.map +1 -1
  33. package/dist/lib/history.d.ts +22 -0
  34. package/dist/lib/history.d.ts.map +1 -1
  35. package/dist/lib/history.js +59 -21
  36. package/dist/lib/history.js.map +1 -1
  37. package/dist/lib/huggingface-provider.d.ts.map +1 -1
  38. package/dist/lib/huggingface-provider.js +11 -3
  39. package/dist/lib/huggingface-provider.js.map +1 -1
  40. package/dist/lib/license.d.ts +76 -0
  41. package/dist/lib/license.d.ts.map +1 -0
  42. package/dist/lib/license.js +141 -0
  43. package/dist/lib/license.js.map +1 -0
  44. package/docs/agentic-harness-primer.md +283 -0
  45. package/docs/conditional-continuation.md +167 -0
  46. package/docs/web-app-agent-guide.md +520 -0
  47. package/examples/web-app-agent/README.md +187 -0
  48. package/examples/web-app-agent/agent/mcp-shim.js +105 -0
  49. package/examples/web-app-agent/gateway.config.example.json +52 -0
  50. package/examples/web-app-agent/package.json +13 -0
  51. package/examples/web-app-agent/public/agent/bridge-mount.js +75 -0
  52. package/examples/web-app-agent/public/agent/chat-client.js +104 -0
  53. package/examples/web-app-agent/public/agent/commands.js +250 -0
  54. package/examples/web-app-agent/public/agent/device-thread.js +48 -0
  55. package/examples/web-app-agent/public/agent/panel.css +107 -0
  56. package/examples/web-app-agent/public/agent/panel.js +369 -0
  57. package/examples/web-app-agent/public/app.js +250 -0
  58. package/examples/web-app-agent/public/index.html +111 -0
  59. package/examples/web-app-agent/server.js +242 -0
  60. package/package.json +7 -3
@@ -0,0 +1,283 @@
1
+ # Cumulus Agentic Harness — A Primer (for OSS models like Kimi)
2
+
3
+ > A plain-language explanation of how Cumulus turns a _plain_ language model into an
4
+ > _agent_ that can read files, run tools, ask questions, and work through multi-step
5
+ > tasks — without relying on the Claude CLI. No code; concepts only.
6
+
7
+ ---
8
+
9
+ ## 1. Why this exists
10
+
11
+ Cumulus originally drove every turn by spawning Anthropic's `claude` command-line
12
+ tool. That CLI hides a lot of machinery: it decides when to call tools, when to stop,
13
+ how to stream output, how to recover from truncation. It only works with Claude.
14
+
15
+ The **agentic harness** is Cumulus owning that machinery itself. It is a single loop
16
+ that can drive _any_ model — Claude through its API, or an open-weight model like
17
+ **Kimi**, **GLM**, or **DeepSeek** served over HuggingFace — through the same
18
+ tool-using, multi-step behavior. The model supplies intelligence; the harness supplies
19
+ the structure that makes that intelligence _agentic_.
20
+
21
+ The one-sentence version:
22
+
23
+ > **The harness repeatedly asks the model "what next?", does whatever the model asks
24
+ > for, feeds the result back, and repeats — until the model says it's finished.**
25
+
26
+ ---
27
+
28
+ ## 2. The core loop
29
+
30
+ Everything centers on one cycle. Think of it as a conversation between the **harness**
31
+ (the orchestrator) and the **model** (the brain):
32
+
33
+ 1. **Ask the model.** The harness sends the system prompt, the conversation so far, and
34
+ the list of available tools. It streams the model's reply back token by token, so the
35
+ user sees text appear live.
36
+ 2. **Look at _why_ the model stopped.** Every model reply ends with a _stop reason_. Two
37
+ matter most:
38
+ - _"I want to use a tool."_ → the harness must run the tool(s) and continue.
39
+ - _"I'm done"_ (or any other reason) → the harness exits the loop and the turn ends.
40
+ 3. **Run the requested tools.** If the model asked to use one or more tools, the harness
41
+ executes them, capturing each result (or error).
42
+ 4. **Feed results back.** The tool results are added to the conversation as the next
43
+ message, exactly as if a user had pasted them in.
44
+ 5. **Repeat from step 1.** The model now sees the results and decides the next move —
45
+ another tool, more tools, or a final answer.
46
+
47
+ This repeats until the model stops asking for tools. A single user message can therefore
48
+ trigger many internal round-trips (read a file → search it → edit it → confirm), all
49
+ invisible to the user except for the streamed narration.
50
+
51
+ ```
52
+ user message
53
+
54
+
55
+ ┌──────────────────────────────────────────────┐
56
+ │ ask model → stream reply → stop reason? │◄────┐
57
+ └──────────────────────────────────────────────┘ │
58
+ │ "use tools" │ "done" │
59
+ ▼ ▼ │
60
+ run tools finish turn │
61
+ │ │
62
+ └──── feed results back as a new message ──────────┘
63
+ ```
64
+
65
+ ### A worked example
66
+
67
+ User asks: _"What PDF library does `read_file` use?"_
68
+
69
+ - **Round 1** — model: "I should read the tool handler." → asks for `read_file`.
70
+ Harness runs it, returns the file summary + chunk list.
71
+ - **Round 2** — model: "The relevant part is chunk 3." → asks for `read_content_chunk`.
72
+ Harness returns that chunk.
73
+ - **Round 3** — model now has what it needs → writes the final answer, stop reason "done".
74
+ Loop exits.
75
+
76
+ Three round-trips, one user-visible answer.
77
+
78
+ ---
79
+
80
+ ## 3. The provider abstraction
81
+
82
+ The loop never talks to a specific model vendor directly. It talks to a **provider** — a
83
+ thin adapter that knows how to (a) send a request and (b) stream back events. Swapping
84
+ the model is just swapping the provider:
85
+
86
+ | Provider | Used for | Notes |
87
+ | -------------------- | ------------------------------------------- | -------------------------------- |
88
+ | **Claude (default)** | `model: "claude"` threads | The historical path. |
89
+ | **HuggingFace** | Open-weight models (Kimi, GLM, DeepSeek, …) | OpenAI-compatible streaming API. |
90
+
91
+ Because the loop only depends on the abstract provider contract, **everything in this
92
+ primer applies identically to Kimi and to Claude.** The harness doesn't special-case the
93
+ model — it special-cases _behaviors_ (truncation, tool support), which is what the rest
94
+ of this document describes.
95
+
96
+ ---
97
+
98
+ ## 4. Tools: eager vs. deferred (the ToolSearch trick)
99
+
100
+ Models pay a token cost for every tool definition you show them. Cumulus exposes _many_
101
+ tools (file reading, content search, inter-agent messaging, email, scheduling, media
102
+ upload, …). Sending every full schema on every turn would waste thousands of tokens each
103
+ round-trip and crowd the context.
104
+
105
+ The harness solves this with a **two-tier tool system**:
106
+
107
+ - **Eager tools (~10).** The handful used constantly. Their full definitions are sent on
108
+ every turn so the model can call them instantly.
109
+ - **Deferred tools (everything else).** Only their _name and one-line description_ are
110
+ listed — not their full schema. They are real, callable tools; they just aren't
111
+ "loaded" yet.
112
+
113
+ When the model wants a deferred tool, it first calls a special tool named **ToolSearch**,
114
+ which returns the full schema for the tool(s) it named. Now the model can call the tool
115
+ for real.
116
+
117
+ ```
118
+ deferred list (cheap): "- send_email: send an email via Resend"
119
+
120
+ ▼ model calls ToolSearch("select:send_email")
121
+ full schema returned (expensive, but only when needed)
122
+
123
+ ▼ model now calls send_email with correct arguments
124
+ ```
125
+
126
+ Two ways to query ToolSearch:
127
+
128
+ - **Exact lookup** — `select:tool_a,tool_b` returns those specific schemas.
129
+ - **Keyword search** — plain words fuzzy-match against tool names and descriptions.
130
+
131
+ **Crucial detail for OSS models:** the deferred schemas are _re-deferred every turn_ —
132
+ they are never permanently accumulated into the context. This keeps the per-turn token
133
+ count **flat** no matter how many tools exist or how long the task runs. For a model with
134
+ a smaller or pricier context window, this is what keeps long agentic sessions affordable.
135
+
136
+ > Practical note: a tool a model "doesn't see" may simply be deferred, not missing.
137
+ > The correct move is always to call ToolSearch first, never to assume a capability is
138
+ > unavailable.
139
+
140
+ ---
141
+
142
+ ## 5. Running tools: concurrent and bounded
143
+
144
+ When the model requests several tools in one turn, the harness runs them **all at once**
145
+ rather than one after another, then collects every result before replying. Independent
146
+ reads and searches finish in parallel instead of stacking up.
147
+
148
+ Each tool result is also **size-bounded**: anything enormous (over ~100 KB) is truncated
149
+ before being handed back to the model, so one giant output can't blow up the context.
150
+ Errors are captured too — a failing tool returns an error message flagged as an error,
151
+ which the model can read and react to, rather than crashing the turn.
152
+
153
+ ---
154
+
155
+ ## 6. Surviving truncation (escalation & stitching)
156
+
157
+ Open-weight models, like all models, have an output-length ceiling per reply. When a
158
+ reply hits that ceiling it gets cut off mid-thought — the stop reason is _"ran out of
159
+ room."_ The harness handles this automatically, and the user ideally never notices. There
160
+ are two cases:
161
+
162
+ **Case A — cut off mid-sentence (plain text).**
163
+ The harness keeps the partial text, then sends a follow-up instruction: _"you were cut
164
+ off; continue exactly where you left off, here are your last ~200 characters, don't
165
+ repeat anything."_ Because the original text stream is still open, the continuation flows
166
+ in seamlessly. This is called **continuation stitching**.
167
+
168
+ **Case B — cut off mid-tool-call.**
169
+ If the cutoff landed inside a tool request, the tool arguments are now incomplete and
170
+ unusable. The harness **discards** that broken attempt entirely and **retries the whole
171
+ turn** with a larger output budget.
172
+
173
+ In both cases the budget grows along a fixed **escalation schedule** — roughly _16k →
174
+ 32k → 65k tokens_ — giving the model progressively more room. Safety caps stop this from
175
+ running forever:
176
+
177
+ | Guardrail | Purpose |
178
+ | ----------------------------------- | ---------------------------------------------------- |
179
+ | Max continuations (~3) | Don't retry/continue endlessly. |
180
+ | Max cumulative output (~65k tokens) | Cap total tokens spent stitching one reply. |
181
+ | Per-model output ceiling | Never request more than the model actually supports. |
182
+
183
+ The escalation never exceeds what the specific model can produce — Kimi's ceiling is
184
+ respected just as Claude's is.
185
+
186
+ ---
187
+
188
+ ## 7. Pausing to ask the user a question
189
+
190
+ Sometimes the model genuinely needs a human decision mid-task (which approach? confirm
191
+ this destructive step?). For this there is a special tool, **AskUserQuestion**.
192
+
193
+ When the model calls it, the loop does something unusual: it **pauses and returns
194
+ control to the caller**, packaging up everything needed to resume later — the
195
+ conversation so far, which question is outstanding, and any _other_ tool results from the
196
+ same turn (those still run normally; only the question blocks progress).
197
+
198
+ The application shows the user the question (single choice, multi-select, or a small
199
+ carousel of questions, optionally with free-text). When the user answers, the harness is
200
+ called again with that answer slotted in as the tool's result, and the loop **resumes
201
+ exactly where it left off** — no work redone, no context lost.
202
+
203
+ So the lifecycle has three states, not two:
204
+
205
+ ```
206
+ running ──► done
207
+
208
+ └──► needs_input ──(user answers)──► running ──► done
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 8. Working step-by-step: "todo mode"
214
+
215
+ Cumulus encourages the model to track multi-step work in `<todo>` checklists. Left
216
+ unchecked, some models — especially eager open-weight ones — try to do _everything_ in
217
+ one giant reply, batching many tool calls and huge output into a single turn. That is
218
+ hard to follow and easy to truncate.
219
+
220
+ When the harness detects todo-tracking is active, it **caps the output budget per turn**
221
+ (to a few thousand tokens). This nudges the model to work in smaller increments: do a
222
+ step, report progress, update the checklist, take the next step. The effect is steadier,
223
+ more legible execution and fewer runaway responses.
224
+
225
+ ---
226
+
227
+ ## 9. What this means for an OSS model like Kimi
228
+
229
+ If you are running Kimi (or GLM, DeepSeek, etc.) inside this harness, here is the mental
230
+ model to operate by:
231
+
232
+ - **You are in a loop.** Your reply isn't the end — if you request a tool, you'll be
233
+ asked again with the result. Don't try to guess tool outputs; ask for the tool and wait.
234
+ - **Stop cleanly when finished.** End without requesting a tool and the loop ends. If you
235
+ keep requesting tools, the loop keeps going (up to a turn limit, ~30).
236
+ - **Use ToolSearch before exotic tools.** Only the ~10 core tools are pre-loaded. For
237
+ anything else, call ToolSearch by exact name or keyword first to get its schema, then
238
+ call it. A tool not shown is _deferred_, not absent.
239
+ - **Emit complete tool calls.** A tool request cut off halfway is thrown away and the
240
+ turn is retried — wasted effort. Prefer smaller, complete requests over one massive
241
+ batch.
242
+ - **Truncation is recoverable, but avoid it.** If you're cut off you'll be asked to
243
+ continue, but working in smaller steps (see todo mode) avoids the round-trips entirely.
244
+ - **Ask when genuinely unsure.** AskUserQuestion is a real escape hatch for decisions you
245
+ shouldn't make alone. It pauses the whole task safely.
246
+ - **Tool calling is the main reliability variable.** The single biggest difference
247
+ between a strong agentic run and a weak one on open models is disciplined, well-formed
248
+ tool use: correct tool names, valid arguments, one clear intent per call.
249
+
250
+ ---
251
+
252
+ ## 10. The whole thing in one picture
253
+
254
+ ```
255
+ ┌─────────────────────────────────────────────┐
256
+ │ THE HARNESS │
257
+ │ │
258
+ user msg ─►│ build prompt + eager tools + deferred list │
259
+ │ │ │
260
+ │ ▼ │
261
+ │ ask model via PROVIDER │ ◄── Claude OR Kimi/GLM/…
262
+ │ (stream tokens out) │
263
+ │ │ │
264
+ │ why did it stop? │
265
+ │ ┌────────────┼─────────────┐ │
266
+ │ tool_use max_tokens done │
267
+ │ │ │ │ │
268
+ │ run tools escalate / finish turn │
269
+ │ (concurrent) stitch / │ │
270
+ │ │ retry exit │
271
+ │ ▼ │ │
272
+ │ feed results back ──┘ │
273
+ │ │ │
274
+ │ └──────────► loop again │
275
+ │ │
276
+ │ (AskUserQuestion → pause → resume later) │
277
+ └─────────────────────────────────────────────┘
278
+ ```
279
+
280
+ **In short:** the model is the brain; the harness is the nervous system. The harness
281
+ makes any capable model — Claude or open-weight — behave as a reliable, tool-using,
282
+ multi-step agent, while quietly handling the unglamorous realities of token budgets,
283
+ truncation, concurrency, and human-in-the-loop pauses.
@@ -0,0 +1,167 @@
1
+ # Conditional Thread Continuation — the Watcher-with-Deadline Pattern
2
+
3
+ **Status:** Adopted convention (Karl, 2026-07-26). No gateway code — this pattern composes
4
+ two primitives that already exist. A more robust gateway-native mechanism is sketched at
5
+ the end for when this outgrows convention.
6
+
7
+ ## The problem
8
+
9
+ A thread kicks off something long-running with an uncertain finish time — a compile, a
10
+ test suite, a deploy, a download. The thread should continue **the moment the outcome is
11
+ known** (success _or_ failure), not after an arbitrary sleep, and not by burning a
12
+ scheduled AI turn every N minutes to poll ("did it finish yet?" cron turns dilute thread
13
+ history — the exact failure class tasks 088–092 fixed).
14
+
15
+ ## The two primitives
16
+
17
+ ### 1. `POST /api/agents/inject` — immediate continuation
18
+
19
+ Any process with a valid gateway API key can inject a message into a thread. The thread
20
+ starts a new turn immediately; if it's mid-stream, the message queues and delivers at
21
+ busy→idle (the task 100 unified queue). Exact shape (verified against
22
+ `server.ts handleAgentInject`):
23
+
24
+ ```bash
25
+ curl -s -X POST http://localhost:8090/api/agents/inject \
26
+ -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
27
+ -d '{"targets": "THREAD_NAME", "sender": "build-watcher", "message": "..."}'
28
+ ```
29
+
30
+ - `targets` (string or array) and `sender` and `message` are required. There is no
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
+ ```
68
+
69
+ For a condition that isn't a process exit (file appears, port answers, log line shows up),
70
+ put an `until` loop before the curl:
71
+
72
+ ```bash
73
+ until <check-command>; do sleep 5; done # e.g. curl -sf http://localhost:3000/health
74
+ ```
75
+
76
+ Guidelines:
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:
102
+
103
+ ```
104
+ schedule_trigger({
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
+ })
110
+ ```
111
+
112
+ Why this matters: the watcher is an unsupervised background process. If the machine
113
+ reboots, the gateway is restarted mid-job, or the script dies, the thread would otherwise
114
+ wait forever. The schedule persists in thread config, so the thread is _guaranteed_ to
115
+ wake — worst case at the deadline, best case the instant the condition fires.
116
+
117
+ ### Step 3 — on wake, cancel the other leg
118
+
119
+ - Watcher fired → first action: `cancel_schedule("build-deadline")`.
120
+ - Deadline fired → check the log/process manually; kill any orphaned watcher
121
+ (`pkill -f build-watcher-marker` if you tagged the command line).
122
+
123
+ ### Anti-pattern: cron polling turns
124
+
125
+ Do **not** use `schedule_trigger` with `trigger: "cron"` to poll a condition every N
126
+ minutes. Each poll burns a full AI turn and appends "checked, nothing yet" noise to
127
+ thread history, degrading retrieval quality for every future turn. Cron is for genuinely
128
+ periodic work (weekly reports, drip follow-ups), not condition-waiting.
129
+
130
+ ## Known limitations of this pattern
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.