@luckydraw/cumulus 1.0.39 → 1.0.40

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 (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/gateway/adapters/webchat.d.ts +3 -0
  3. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  4. package/dist/gateway/adapters/webchat.js +70 -0
  5. package/dist/gateway/adapters/webchat.js.map +1 -1
  6. package/dist/gateway/config.d.ts +16 -2
  7. package/dist/gateway/config.d.ts.map +1 -1
  8. package/dist/gateway/config.js +12 -6
  9. package/dist/gateway/config.js.map +1 -1
  10. package/dist/gateway/daemon.d.ts +1 -0
  11. package/dist/gateway/daemon.d.ts.map +1 -1
  12. package/dist/gateway/daemon.js +4 -0
  13. package/dist/gateway/daemon.js.map +1 -1
  14. package/dist/gateway/one-shot-model.d.ts +92 -0
  15. package/dist/gateway/one-shot-model.d.ts.map +1 -0
  16. package/dist/gateway/one-shot-model.js +191 -0
  17. package/dist/gateway/one-shot-model.js.map +1 -0
  18. package/dist/gateway/server.d.ts +3 -0
  19. package/dist/gateway/server.d.ts.map +1 -1
  20. package/dist/gateway/server.js +4 -0
  21. package/dist/gateway/server.js.map +1 -1
  22. package/dist/gateway/stall-detection.d.ts +19 -50
  23. package/dist/gateway/stall-detection.d.ts.map +1 -1
  24. package/dist/gateway/stall-detection.js +27 -190
  25. package/dist/gateway/stall-detection.js.map +1 -1
  26. package/dist/gateway/static/widget.js +139 -18
  27. package/dist/gateway/tasks-refresh.d.ts +89 -0
  28. package/dist/gateway/tasks-refresh.d.ts.map +1 -0
  29. package/dist/gateway/tasks-refresh.js +224 -0
  30. package/dist/gateway/tasks-refresh.js.map +1 -0
  31. package/dist/gateway/tasks.d.ts +18 -0
  32. package/dist/gateway/tasks.d.ts.map +1 -1
  33. package/dist/gateway/tasks.js +42 -7
  34. package/dist/gateway/tasks.js.map +1 -1
  35. package/dist/gateway/worker.d.ts +86 -0
  36. package/dist/gateway/worker.d.ts.map +1 -0
  37. package/dist/gateway/worker.js +0 -0
  38. package/dist/gateway/worker.js.map +1 -0
  39. package/package.json +1 -1
@@ -16,57 +16,17 @@
16
16
  * the worse failure (Karl, 2026-08-30).
17
17
  *
18
18
  * The classifier runs on WHATEVER model the operator configured
19
- * (`stallClassifierModel`), not just a Claude sub-model (task 165). It routes
20
- * through `resolveModelProvider` — the same single source of truth every other
21
- * model in the gateway uses — and branches: a Claude CLI sub-model runs as a
22
- * one-off `claude --print --model <id>` call; any other catalog model runs as a
23
- * one-shot HTTP `chat()` through the provider the resolver picks. The prompt
24
- * and the fail-closed contract are identical on both branches.
19
+ * (`stallClassifierModel`), not just a Claude sub-model (task 165), via
20
+ * `runOneShotModel` — the one way the gateway itself calls a model (task 179).
21
+ * This module owns the PROMPT and the fail-closed reading of the answer; which
22
+ * provider runs it, and how, lives in `one-shot-model.ts`.
25
23
  */
24
+ import { type OneShotModelContext } from './one-shot-model.js';
26
25
  export declare function hasUncheckedTodo(text: string): boolean;
27
26
  /** Last `n` sentences of a message — the classifier only needs the tail, not the whole thing. */
28
27
  export declare function lastSentences(text: string, n?: number): string;
29
28
  /** The single dangling sentence to quote back in the nudge message. */
30
29
  export declare function extractDanglingSentence(text: string): string;
31
- /**
32
- * Everything the classifier needs to decide WHICH provider runs the configured
33
- * model and HOW to reach it. Mirrors the fields `server.ts` already threads into
34
- * every turn's `sendMessage`, so the classifier resolves its model exactly the way
35
- * a turn does — no second notion of "how do I run a non-Claude model."
36
- */
37
- export interface StallClassifierContext {
38
- /** Direct-provider catalog (HF / OpenAI / custom entries). */
39
- models?: Array<{
40
- id: string;
41
- provider?: string;
42
- contextWindow?: number;
43
- }>;
44
- /** Claude CLI sub-model catalog (Opus / Sonnet / Haiku variants). */
45
- claudeModels?: Array<{
46
- id: string;
47
- }>;
48
- hfApiKey?: string;
49
- openaiApiKey?: string;
50
- customProviders?: Array<{
51
- id: string;
52
- baseUrl: string;
53
- apiKey?: string;
54
- }>;
55
- }
56
- /**
57
- * Resolve the configured stall-classifier model id against BOTH catalogs.
58
- *
59
- * An id naming a `claudeModels[]` entry runs as a Claude CLI sub-model; an id
60
- * naming a `models[]` entry runs on that entry's direct provider (task 165). An
61
- * id that names nothing in either (absent, blank, or stale after a catalog edit)
62
- * falls back to `"haiku"` rather than failing closed, since the classifier is a
63
- * best-effort fallback signal, not a correctness gate.
64
- */
65
- export declare function resolveStallClassifierModel(stallClassifierModel: string | undefined, claudeModels?: Array<{
66
- id: string;
67
- }>, models?: Array<{
68
- id: string;
69
- }>): string;
70
30
  export interface ClassifyStallOptions {
71
31
  timeoutMs?: number;
72
32
  /**
@@ -80,15 +40,24 @@ export interface ClassifyStallOptions {
80
40
  * routed through `resolveModelProvider` and run on whatever provider that
81
41
  * names — a Claude CLI sub-model or a direct-provider HTTP model (task 165).
82
42
  */
83
- context?: StallClassifierContext;
43
+ context?: OneShotModelContext;
84
44
  }
85
45
  /**
86
46
  * Cheap-model classification for the "trails off with no `<todo>` at all" case.
87
47
  *
88
- * Routes the configured model through `resolveModelProvider` and runs it on
89
- * whatever provider that names: a Claude CLI sub-model as a one-off
90
- * `claude --print --model <id>` call, any other catalog model as a one-shot
91
- * HTTP `chat()` (task 165). Fails closed on every branch.
48
+ * The provider branch lives in `runOneShotModel` (task 179); what stays here is
49
+ * the part that is the classifier's own the prompt, the tail-of-message snippet
50
+ * it runs on, and the fail-closed reading of the answer.
51
+ *
52
+ * `reasoningEffort: 'low'` (task 166) because the classification is a trivial
53
+ * binary read of a few sentences and must not deliberate; `CLASSIFIER_MAX_TOKENS`
54
+ * (task 167) because on both HTTP providers that budget bounds REASONING +
55
+ * CONTENT together, so it has to hold the whole low-effort thinking pass before
56
+ * the one-word verdict can be emitted at all.
57
+ *
58
+ * Fails closed on every branch: a `null` answer (spawn failure, non-zero exit,
59
+ * timeout, provider throw) and a non-"YES" answer are both "no nudge", because
60
+ * prodding a thread that correctly finished is the worse failure.
92
61
  */
93
62
  export declare function classifyStall(text: string, opts?: ClassifyStallOptions): Promise<boolean>;
94
63
  /** Is the last message from the thread's own `<todo>`/classifier signals a stall? */
@@ -1 +1 @@
1
- {"version":3,"file":"stall-detection.d.ts","sourceRoot":"","sources":["../../src/gateway/stall-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAYH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUtD;AAkCD,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,SAAI,GAAG,MAAM,CAEzD;AAED,uEAAuE;AACvE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG5D;AAeD;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,oBAAoB,EAAE,MAAM,GAAG,SAAS,EACxC,YAAY,CAAC,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,EACpC,MAAM,CAAC,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7B,MAAM,CAMR;AAoBD,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,sBAAsB,CAAC;CAClC;AAwJD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAqClB;AAED,qFAAqF;AACrF,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,OAAO,CAAC,OAAO,CAAC,CAG/F;AAID;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAC;IAClB,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GAAG,aAAa,CAKhB;AAID,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,qEAAqE;AACrE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,cAAc,EAAE,GAAG,SAAS,EAClC,SAAS,EAAE,mBAAmB,EAAE,GAAG,SAAS,EAC5C,GAAG,GAAE,MAAmB,GACvB,MAAM,GAAG,IAAI,CAgBf"}
1
+ {"version":3,"file":"stall-detection.d.ts","sourceRoot":"","sources":["../../src/gateway/stall-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAMhF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUtD;AAkCD,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,SAAI,GAAG,MAAM,CAEzD;AAED,uEAAuE;AACvE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG5D;AA+BD,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAalB;AAED,qFAAqF;AACrF,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,OAAO,CAAC,OAAO,CAAC,CAG/F;AAID;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAC;IAClB,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GAAG,aAAa,CAKhB;AAID,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,qEAAqE;AACrE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,cAAc,EAAE,GAAG,SAAS,EAClC,SAAS,EAAE,mBAAmB,EAAE,GAAG,SAAS,EAC5C,GAAG,GAAE,MAAmB,GACvB,MAAM,GAAG,IAAI,CAgBf"}
@@ -16,17 +16,12 @@
16
16
  * the worse failure (Karl, 2026-08-30).
17
17
  *
18
18
  * The classifier runs on WHATEVER model the operator configured
19
- * (`stallClassifierModel`), not just a Claude sub-model (task 165). It routes
20
- * through `resolveModelProvider` — the same single source of truth every other
21
- * model in the gateway uses — and branches: a Claude CLI sub-model runs as a
22
- * one-off `claude --print --model <id>` call; any other catalog model runs as a
23
- * one-shot HTTP `chat()` through the provider the resolver picks. The prompt
24
- * and the fail-closed contract are identical on both branches.
19
+ * (`stallClassifierModel`), not just a Claude sub-model (task 165), via
20
+ * `runOneShotModel` — the one way the gateway itself calls a model (task 179).
21
+ * This module owns the PROMPT and the fail-closed reading of the answer; which
22
+ * provider runs it, and how, lives in `one-shot-model.ts`.
25
23
  */
26
- import { spawn } from 'child_process';
27
- import { resolveModelProvider } from '../lib/gateway.js';
28
- import { HuggingFaceProvider } from '../lib/huggingface-provider.js';
29
- import { OpenAIResponsesProvider } from '../lib/openai-responses-provider.js';
24
+ import { runOneShotModel } from './one-shot-model.js';
30
25
  /** Last (authoritative, full-replacement) `<todo>` block still has an open `[ ]` item. */
31
26
  const TODO_BLOCK_RE = /<todo>([\s\S]*?)<\/todo>/g;
32
27
  const UNCHECKED_ITEM_RE = /\[\s\]/;
@@ -95,25 +90,6 @@ const STALL_CLASSIFIER_PROMPT = (snippet) => `You are checking whether an AI ass
95
90
  `message reads as a completed thought, a finished answer, a question back to the user, an ` +
96
91
  `offer awaiting the user's decision, or anything you are not certain about, answer NO.\n\n` +
97
92
  `Respond with exactly one word: YES or NO.`;
98
- /**
99
- * Resolve the configured stall-classifier model id against BOTH catalogs.
100
- *
101
- * An id naming a `claudeModels[]` entry runs as a Claude CLI sub-model; an id
102
- * naming a `models[]` entry runs on that entry's direct provider (task 165). An
103
- * id that names nothing in either (absent, blank, or stale after a catalog edit)
104
- * falls back to `"haiku"` rather than failing closed, since the classifier is a
105
- * best-effort fallback signal, not a correctness gate.
106
- */
107
- export function resolveStallClassifierModel(stallClassifierModel, claudeModels, models) {
108
- const id = stallClassifierModel?.trim();
109
- if (!id)
110
- return 'haiku';
111
- if (claudeModels?.some(entry => entry.id === id))
112
- return id;
113
- if (models?.some(entry => entry.id === id))
114
- return id;
115
- return 'haiku';
116
- }
117
93
  // ─── Classifier execution ───────────────────────────────────────────────────────
118
94
  /**
119
95
  * Output budget for the HTTP classifier call (task 167).
@@ -131,175 +107,36 @@ export function resolveStallClassifierModel(stallClassifierModel, claudeModels,
131
107
  */
132
108
  const CLASSIFIER_MAX_TOKENS = 512;
133
109
  /**
134
- * The Claude CLI branch: a one-off `claude --print --model <model>` call, not a
135
- * full agentic turn. Fails closed: any error, non-zero exit, timeout, or
136
- * non-"YES" response resolves to `false` (no nudge).
110
+ * Cheap-model classification for the "trails off with no `<todo>` at all" case.
137
111
  *
138
- * The prompt goes in as PLAIN TEXT on stdin not `--input-format stream-json`.
139
- * That flag requires a matching `--output-format stream-json --verbose`, and without
140
- * it the CLI errors before reaching the model (`Error: --input-format=stream-json
141
- * requires output-format=stream-json.`), so the classifier would fail closed on every
142
- * call and the fallback signal would be dead. Plain text on stdin is the form that
143
- * actually works and sidesteps ARG_MAX for long prompts.
144
- */
145
- async function classifyViaClaudeCli(text, model, timeoutMs) {
146
- const snippet = lastSentences(text, 6);
147
- if (!snippet)
148
- return false;
149
- return new Promise(resolve => {
150
- const args = ['--print', '--model', model];
151
- const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('CLAUDE') && key !== 'CLAUDECODE'));
152
- let claude;
153
- try {
154
- claude = spawn('claude', args, {
155
- stdio: ['pipe', 'pipe', 'pipe'],
156
- env: cleanEnv,
157
- shell: process.platform === 'win32',
158
- windowsHide: true,
159
- });
160
- }
161
- catch {
162
- resolve(false);
163
- return;
164
- }
165
- // Plain text on stdin (see doc comment) — no JSON envelope, no --input-format.
166
- claude.stdin?.write(STALL_CLASSIFIER_PROMPT(snippet));
167
- claude.stdin?.end();
168
- let stdout = '';
169
- let settled = false;
170
- const finish = (result) => {
171
- if (settled)
172
- return;
173
- settled = true;
174
- clearTimeout(timer);
175
- resolve(result);
176
- };
177
- const timer = setTimeout(() => {
178
- claude.kill();
179
- finish(false);
180
- }, timeoutMs);
181
- timer.unref?.();
182
- claude.stdout?.on('data', (data) => {
183
- stdout += data.toString();
184
- });
185
- claude.on('close', code => {
186
- if (code !== 0) {
187
- finish(false);
188
- return;
189
- }
190
- finish(/^YES\b/i.test(stdout.trim()));
191
- });
192
- claude.on('error', () => finish(false));
193
- });
194
- }
195
- /**
196
- * The direct-provider branch (task 165): a one-shot HTTP `chat()` through the
197
- * provider `resolveModelProvider` picked for the configured model. No tools, no
198
- * system prompt beyond the classifier prompt. The answer is one word, but the
199
- * `maxTokens` budget must be large enough to hold a reasoning model's thinking pass
200
- * first (task 167). Fails closed exactly like the CLI branch: any throw, timeout, or
201
- * non-"YES" response resolves to `false`.
112
+ * The provider branch lives in `runOneShotModel` (task 179); what stays here is
113
+ * the part that is the classifier's own the prompt, the tail-of-message snippet
114
+ * it runs on, and the fail-closed reading of the answer.
202
115
  *
203
- * Task 166 two changes so a REASONING model can actually emit the verdict:
204
- * - `reasoningEffort: 'low'`. The classification is a trivial binary read of a
205
- * few sentences; it must not deliberate. At the default effort a reasoning
206
- * model spends its whole output budget thinking and never reaches the content
207
- * phase, so the answer is empty and this fails closed on every call — the
208
- * detector was structurally dead for reasoning models.
209
- * - The verdict is parsed from the streamed `text` events, not `response.content`.
210
- * Both HTTP providers emit `text` events for CONTENT only — reasoning is
211
- * accumulated separately and never streamed as a `text` event. But
212
- * `response.content` folds reasoning in as a last-resort text block
213
- * (`fullText || reasoningText`), so reading the blocks can hand the parser the
214
- * model's private scratch paper. The `text` events are the channel that is
215
- * guaranteed to be the actual answer; if none ever arrive, the answer is empty
216
- * and we fail closed, exactly as before.
116
+ * `reasoningEffort: 'low'` (task 166) because the classification is a trivial
117
+ * binary read of a few sentences and must not deliberate; `CLASSIFIER_MAX_TOKENS`
118
+ * (task 167) because on both HTTP providers that budget bounds REASONING +
119
+ * CONTENT together, so it has to hold the whole low-effort thinking pass before
120
+ * the one-word verdict can be emitted at all.
217
121
  *
218
- * Task 167 the third, load-bearing half. `reasoningEffort: 'low'` alone was NOT
219
- * sufficient: on both HTTP providers `maxTokens` bounds REASONING + CONTENT together,
220
- * and the pre-167 value (16) was smaller than even a low-effort thinking pass, so the
221
- * model still ran out of budget mid-reasoning and emitted no `text` event. The budget
222
- * had to grow to `CLASSIFIER_MAX_TOKENS` (512) to hold the thinking pass AND the
223
- * verdict. (The 166 comment originally claimed capping effort at `low` was what let
224
- * the model "finish thinking inside `maxTokens`" — it did not; the budget was the
225
- * binding constraint all along.)
122
+ * Fails closed on every branch: a `null` answer (spawn failure, non-zero exit,
123
+ * timeout, provider throw) and a non-"YES" answer are both "no nudge", because
124
+ * prodding a thread that correctly finished is the worse failure.
226
125
  */
227
- async function classifyViaHttp(text, modelId, resolved, timeoutMs) {
126
+ export async function classifyStall(text, opts = {}) {
228
127
  const snippet = lastSentences(text, 6);
229
128
  if (!snippet)
230
129
  return false;
231
- const provider = resolved.wireProtocol === 'openai-responses'
232
- ? new OpenAIResponsesProvider(resolved.apiKey, resolved.baseUrl)
233
- : new HuggingFaceProvider(resolved.apiKey, modelId, resolved.baseUrl);
234
- const controller = new AbortController();
235
- const timer = setTimeout(() => controller.abort(), timeoutMs);
236
- timer.unref?.();
237
- let answer = '';
238
- try {
239
- await provider.chat({
240
- model: modelId,
241
- system: [],
242
- messages: [{ role: 'user', content: STALL_CLASSIFIER_PROMPT(snippet) }],
243
- tools: [],
244
- maxTokens: CLASSIFIER_MAX_TOKENS,
245
- reasoningEffort: 'low',
246
- signal: controller.signal,
247
- onEvent: event => {
248
- if (event.type === 'text' && event.text)
249
- answer += event.text;
250
- },
251
- });
252
- return /^YES\b/i.test(answer.trim());
253
- }
254
- catch {
255
- // Any provider error, abort, or malformed response fails closed to "no nudge".
256
- return false;
257
- }
258
- finally {
259
- clearTimeout(timer);
260
- }
261
- }
262
- /**
263
- * Cheap-model classification for the "trails off with no `<todo>` at all" case.
264
- *
265
- * Routes the configured model through `resolveModelProvider` and runs it on
266
- * whatever provider that names: a Claude CLI sub-model as a one-off
267
- * `claude --print --model <id>` call, any other catalog model as a one-shot
268
- * HTTP `chat()` (task 165). Fails closed on every branch.
269
- */
270
- export async function classifyStall(text, opts = {}) {
271
- const timeoutMs = opts.timeoutMs ?? 10000;
272
- const ctx = opts.context;
273
- // No context: preserve the task-164 behavior — the configured id (or "haiku")
274
- // runs directly as a Claude CLI sub-model. This is what unit tests and any
275
- // caller without a catalog rely on.
276
- if (!ctx) {
277
- const model = opts.model?.trim() || 'haiku';
278
- return classifyViaClaudeCli(text, model, timeoutMs);
279
- }
280
- const modelId = resolveStallClassifierModel(opts.model, ctx.claudeModels, ctx.models);
281
- // A Claude CLI sub-model runs on the CLI — detected by membership in claudeModels[],
282
- // plus the "haiku" fallback (a valid CLI alias). These are NEVER passed to
283
- // resolveModelProvider: that resolver's contract is "claude" or a direct-provider id,
284
- // and feeding it a Claude sub-model id would fall through to the HF branch and
285
- // misroute a Claude model over HTTP when an HF key is present.
286
- const isClaudeSub = modelId === 'haiku' || ctx.claudeModels?.some(entry => entry.id === modelId);
287
- if (isClaudeSub) {
288
- return classifyViaClaudeCli(text, modelId, timeoutMs);
289
- }
290
- // A direct-provider model: route through the same single source of truth every turn
291
- // uses (task 118/147). No second notion of "how do I run a non-Claude model."
292
- const resolved = resolveModelProvider(modelId, ctx.models, {
293
- hfApiKey: ctx.hfApiKey,
294
- openaiApiKey: ctx.openaiApiKey,
295
- customProviders: ctx.customProviders,
130
+ const answer = await runOneShotModel(STALL_CLASSIFIER_PROMPT(snippet), {
131
+ model: opts.model,
132
+ context: opts.context,
133
+ timeoutMs: opts.timeoutMs ?? 10000,
134
+ maxTokens: CLASSIFIER_MAX_TOKENS,
135
+ reasoningEffort: 'low',
296
136
  });
297
- if (!resolved.isAgentic) {
298
- // Gate closed (no credential for the resolved provider) — degrade to the default
299
- // Claude sub-model rather than spawning a doomed HTTP loop.
300
- return classifyViaClaudeCli(text, 'haiku', timeoutMs);
301
- }
302
- return classifyViaHttp(text, modelId, resolved, timeoutMs);
137
+ if (answer === null)
138
+ return false;
139
+ return /^YES\b/i.test(answer.trim());
303
140
  }
304
141
  /** Is the last message from the thread's own `<todo>`/classifier signals a stall? */
305
142
  export async function isStalled(text, opts = {}) {
@@ -1 +1 @@
1
- {"version":3,"file":"stall-detection.js","sourceRoot":"","sources":["../../src/gateway/stall-detection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stall-detection.js","sourceRoot":"","sources":["../../src/gateway/stall-detection.ts"],"names":[],"mappings":"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"}
@@ -144,6 +144,9 @@
144
144
  // "haiku" for an unmatched id anyway.
145
145
  stallClassifierModel:
146
146
  typeof config.stallClassifierModel === 'string' ? config.stallClassifierModel : '',
147
+ // Task 179. Same verbatim rule again; the gateway's resolveGatewayModel falls
148
+ // back to "haiku" for an unmatched id.
149
+ workerModel: typeof config.workerModel === 'string' ? config.workerModel : '',
147
150
  };
148
151
  }
149
152
 
@@ -208,6 +211,9 @@
208
211
  // Task 164: '' means "haiku" and must reach the server as an explicit null (clear),
209
212
  // not as absent — absent would leave a previously-set classifier model untouched.
210
213
  stallClassifierModel: state.stallClassifierModel ? state.stallClassifierModel : null,
214
+ // Task 179: same explicit-null rule — '' means "haiku", and absent would
215
+ // leave a previously-set worker model untouched.
216
+ workerModel: state.workerModel ? state.workerModel : null,
211
217
  // Registry travels without credentials — addCredentialPatch fills those
212
218
  // in, so an untouched key round-trips as absent and the server preserves it.
213
219
  customProviders: (Array.isArray(state.customProviders) ? state.customProviders : []).map(
@@ -1975,13 +1981,16 @@
1975
1981
  '}',
1976
1982
  '.cumulus-task-dep button:hover { border-color: #666; color: #eee; }',
1977
1983
 
1978
- /* Selection bar — task 178: above the list, and only while rows are ticked */
1984
+ /* Toolbar — task 178 put the selection actions above the list; task 179 gave
1985
+ the bar something to show at zero selected, so it is now always present. */
1979
1986
  '.cumulus-tasks-selbar {',
1980
1987
  ' display: flex; align-items: center; gap: 6px; flex-shrink: 0;',
1981
1988
  ' padding: 0.45em 0.6em 0.45em 0.85em; background: #2d2d2d;',
1982
1989
  ' border-bottom: 1px solid #3a3a3a;',
1983
1990
  '}',
1984
1991
  '.cumulus-tasks-selcount { font-size: 11.5px; color: #aaa; margin-right: auto; }',
1992
+ '.cumulus-tasks-refresh-note { font-size: 11.5px; color: #888; margin-right: auto; }',
1993
+ '.cumulus-tasks-action--refresh:disabled { opacity: 0.55; cursor: default; }',
1985
1994
  '.cumulus-tasks-action {',
1986
1995
  ' background: #2a2a2a; border: 1px solid #3f3f3f; color: #ddd;',
1987
1996
  ' border-radius: 4px; padding: 0.28em 0.6em; cursor: pointer; font-size: 11px;',
@@ -6688,6 +6697,25 @@
6688
6697
  stallCard.appendChild(stallSelect);
6689
6698
  body.appendChild(stallCard);
6690
6699
 
6700
+ // Task 179: background workers get their own dial rather than sharing the
6701
+ // classifier's. The jobs differ in difficulty — that one is a yes/no read of
6702
+ // three sentences, this one rewrites a whole task file and judges every
6703
+ // task's state — so the cheap model that is plenty for the first may not be
6704
+ // for the second.
6705
+ var workerCard = document.createElement('div');
6706
+ workerCard.className = 'cumulus-settings-default-card';
6707
+ var workerCardLabel = document.createElement('label');
6708
+ workerCardLabel.textContent = 'Background workers use';
6709
+ var workerSelect = document.createElement('select');
6710
+ workerSelect.className = 'cumulus-settings-select';
6711
+ workerSelect.setAttribute('data-testid', 'webchat-gw-worker-select');
6712
+ workerSelect.addEventListener('change', function () {
6713
+ gwState.workerModel = workerSelect.value;
6714
+ });
6715
+ workerCard.appendChild(workerCardLabel);
6716
+ workerCard.appendChild(workerSelect);
6717
+ body.appendChild(workerCard);
6718
+
6691
6719
  var modelsLabel = document.createElement('div');
6692
6720
  modelsLabel.className = 'cumulus-settings-section-label';
6693
6721
  modelsLabel.textContent = 'Model catalog';
@@ -7327,6 +7355,18 @@
7327
7355
  if (stallSelect.value !== (gwState.stallClassifierModel || '')) stallSelect.value = '';
7328
7356
  }
7329
7357
 
7358
+ // Task 179 — same rule, same fallback, its own setting.
7359
+ function renderWorkerSelect() {
7360
+ workerSelect.innerHTML = '';
7361
+ var workerHaiku = document.createElement('option');
7362
+ workerHaiku.value = '';
7363
+ workerHaiku.textContent = 'Haiku (default)';
7364
+ workerSelect.appendChild(workerHaiku);
7365
+ fillModelOptions(workerSelect);
7366
+ workerSelect.value = gwState.workerModel || '';
7367
+ if (workerSelect.value !== (gwState.workerModel || '')) workerSelect.value = '';
7368
+ }
7369
+
7330
7370
  // Rebuild the provider list from state before anything reads it, so a
7331
7371
  // provider added or removed this session reaches the cards, the default
7332
7372
  // dropdown and the add-model select in the same pass (task 147).
@@ -7351,6 +7391,7 @@
7351
7391
  renderDefaultSelect();
7352
7392
  renderVoiceSelect();
7353
7393
  renderStallSelect();
7394
+ renderWorkerSelect();
7354
7395
  renderGwModels();
7355
7396
  }
7356
7397
 
@@ -9263,7 +9304,27 @@
9263
9304
  var selbar = document.createElement('div');
9264
9305
  selbar.className = 'cumulus-tasks-selbar';
9265
9306
  selbar.setAttribute('data-testid', 'webchat-tasks-selbar');
9266
- selbar.style.display = 'none';
9307
+
9308
+ // Task 179 — Refresh occupies the toolbar whenever nothing is ticked. It is
9309
+ // the only control here that acts on the WHOLE file rather than a selection,
9310
+ // which is why it swaps out entirely the moment a row is ticked rather than
9311
+ // sitting alongside actions that mean something narrower.
9312
+ var refreshNote = document.createElement('span');
9313
+ refreshNote.className = 'cumulus-tasks-refresh-note';
9314
+ refreshNote.setAttribute('data-testid', 'webchat-tasks-refresh-note');
9315
+ refreshNote.textContent = '';
9316
+ selbar.appendChild(refreshNote);
9317
+
9318
+ var refreshBtn = taskActionButton('Refresh', 'refresh', function () {
9319
+ requestTasksRefresh(drawer, threadName);
9320
+ });
9321
+ refreshBtn.setAttribute(
9322
+ 'title',
9323
+ 'Convert this file to the current format and re-check every task’s state. ' +
9324
+ 'Commits a snapshot to git first.'
9325
+ );
9326
+ selbar.appendChild(refreshBtn);
9327
+
9267
9328
  var selcount = document.createElement('span');
9268
9329
  selcount.className = 'cumulus-tasks-selcount';
9269
9330
  selcount.setAttribute('data-testid', 'webchat-tasks-selcount');
@@ -9281,16 +9342,14 @@
9281
9342
  });
9282
9343
  selbar.appendChild(commentBtn);
9283
9344
 
9284
- selbar.appendChild(
9285
- taskActionButton('Closed ✓', 'closed', function () {
9286
- dispositionSelectedTasks(drawer, threadName, 'closed');
9287
- })
9288
- );
9289
- selbar.appendChild(
9290
- taskActionButton('Archive', 'archive', function () {
9291
- dispositionSelectedTasks(drawer, threadName, 'archived');
9292
- })
9293
- );
9345
+ var closedBtn = taskActionButton('Closed ✓', 'closed', function () {
9346
+ dispositionSelectedTasks(drawer, threadName, 'closed');
9347
+ });
9348
+ selbar.appendChild(closedBtn);
9349
+ var archiveBtn = taskActionButton('Archive', 'archive', function () {
9350
+ dispositionSelectedTasks(drawer, threadName, 'archived');
9351
+ });
9352
+ selbar.appendChild(archiveBtn);
9294
9353
 
9295
9354
  var clearBtn = document.createElement('button');
9296
9355
  clearBtn.className = 'cumulus-tasks-selclear';
@@ -9314,6 +9373,10 @@
9314
9373
  drawer._selbar = selbar;
9315
9374
  drawer._selcount = selcount;
9316
9375
  drawer._panel = panel;
9376
+ drawer._refreshBtn = refreshBtn;
9377
+ drawer._refreshNote = refreshNote;
9378
+ // The selection half of the toolbar, hidden together at zero selected.
9379
+ drawer._selGroup = [selcount, commentBtn, closedBtn, archiveBtn, clearBtn];
9317
9380
 
9318
9381
  panel.appendChild(drawer);
9319
9382
  if (connection) connection.send({ type: 'tasks_list', threadName: threadName });
@@ -9467,10 +9530,13 @@
9467
9530
  body.innerHTML = '';
9468
9531
 
9469
9532
  if (data.missing) {
9533
+ // The un-migrated case is exactly what Refresh is for, so say so here
9534
+ // rather than leaving the button above looking decorative (task 179).
9470
9535
  body.innerHTML =
9471
- '<div class="cumulus-tasks-empty">This thread has no tasks file.<br><br>' +
9472
- 'Add a <code>Tasks.md</code> carrying the <code>cumulus:tasks</code> marker to the ' +
9473
- 'thread’s always-include list, and it will appear here.</div>';
9536
+ '<div class="cumulus-tasks-empty">This thread has no tasks file in the current ' +
9537
+ 'format.<br><br>If it already has a <code>Tasks.md</code> in its always-include ' +
9538
+ 'list, press <b>Refresh</b> to convert it the file is committed to git first. ' +
9539
+ 'Otherwise add one, and it will appear here.</div>';
9474
9540
  drawer._selected = {};
9475
9541
  updateTaskSelection(drawer);
9476
9542
  return;
@@ -9590,15 +9656,58 @@
9590
9656
  });
9591
9657
  }
9592
9658
 
9659
+ // The toolbar shows exactly one of two things. At zero selected it is
9660
+ // Refresh, which acts on the whole file; with rows ticked it is the count and
9661
+ // the actions that act on those rows. Task 178 hid the bar entirely at zero
9662
+ // because a permanent "0 selected" strip answered a question nobody asked —
9663
+ // 179 gives that space a real use, so the bar stays and its contents swap.
9593
9664
  function updateTaskSelection(drawer) {
9594
9665
  var n = Object.keys(drawer._selected).length;
9595
9666
  drawer._selcount.textContent = n + ' selected';
9596
- // The bar is absent at zero rather than disabled: a permanent "0 selected"
9597
- // strip costs a row of list on every open and answers a question nobody asked.
9598
- drawer._selbar.style.display = n === 0 ? 'none' : '';
9667
+ var group = drawer._selGroup || [];
9668
+ for (var i = 0; i < group.length; i++) group[i].style.display = n === 0 ? 'none' : '';
9669
+ drawer._refreshBtn.style.display = n === 0 ? '' : 'none';
9670
+ drawer._refreshNote.style.display = n === 0 ? '' : 'none';
9599
9671
  if (n === 0) closeTaskComment(drawer, 'selection');
9600
9672
  }
9601
9673
 
9674
+ // Task 179. The button is disabled optimistically rather than on the server's
9675
+ // `running` push: the round-trip is long enough to double-click through, and a
9676
+ // second press is REFUSED by the server, not queued — so an un-disabled button
9677
+ // would just produce an error message for a press the user meant.
9678
+ function requestTasksRefresh(drawer, threadName) {
9679
+ if (drawer._refreshBtn.disabled) return;
9680
+ setTasksRefreshBusy(drawer, true, 'Refreshing…');
9681
+ drawer._errorEl.textContent = '';
9682
+ drawer._errorEl.style.display = 'none';
9683
+ if (connection) connection.send({ type: 'tasks_refresh', threadName: threadName });
9684
+ }
9685
+
9686
+ function setTasksRefreshBusy(drawer, busy, note) {
9687
+ drawer._refreshBtn.disabled = !!busy;
9688
+ drawer._refreshBtn.textContent = busy ? 'Refreshing…' : 'Refresh';
9689
+ drawer._refreshNote.textContent = note || '';
9690
+ }
9691
+
9692
+ // A worker's terminal status. `failed` lands in the same error strip a rejected
9693
+ // drag uses — one place in the drawer where the file's own truth is reported,
9694
+ // whatever refused the write.
9695
+ function applyWorkerStatus(panel, threadName, data) {
9696
+ var drawer = panel.querySelector('.cumulus-tasks-drawer');
9697
+ if (!drawer || drawer._threadName !== threadName) return;
9698
+ if (data.kind !== 'tasks-refresh') return;
9699
+
9700
+ if (data.state === 'running') {
9701
+ setTasksRefreshBusy(drawer, true, 'Refreshing…');
9702
+ return;
9703
+ }
9704
+ setTasksRefreshBusy(drawer, false, data.state === 'done' ? data.detail || '' : '');
9705
+ if (data.state === 'failed') {
9706
+ drawer._errorEl.textContent = data.detail || 'Refresh failed.';
9707
+ drawer._errorEl.style.display = '';
9708
+ }
9709
+ }
9710
+
9602
9711
  function taskActionButton(label, name, onClick) {
9603
9712
  var btn = document.createElement('button');
9604
9713
  btn.className = 'cumulus-tasks-action cumulus-tasks-action--' + name;
@@ -10839,6 +10948,18 @@
10839
10948
  }
10840
10949
  break;
10841
10950
 
10951
+ // A gateway-side background worker changed state (task 179). Deliberately
10952
+ // NOT `thread_activity`: a worker is not a turn, so it must not light the
10953
+ // sidebar dots or claim the thread is busy.
10954
+ case 'worker_status':
10955
+ if (data.threadName) {
10956
+ var workerPanel = document.querySelector(
10957
+ '[data-thread-panel="' + data.threadName + '"]'
10958
+ );
10959
+ if (workerPanel) applyWorkerStatus(workerPanel, data.threadName, data);
10960
+ }
10961
+ break;
10962
+
10842
10963
  case 'include_added':
10843
10964
  case 'include_removed':
10844
10965
  // List already updated by the handler — no extra action needed