@luckydraw/cumulus 1.0.27 → 1.0.28

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.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Stall detection (task 164).
3
+ *
4
+ * A thread that ends a turn on a promised-but-untaken next action ("Let me
5
+ * look at how X renders.") with nothing after it is not empty (task 150/156
6
+ * doesn't catch it) and may never have armed a `schedule_trigger` (task 156
7
+ * only helps if the model remembers to). This module supplies the two
8
+ * detection signals `server.ts` uses on the busy→idle edge:
9
+ *
10
+ * 1. `hasUncheckedTodo` — free, structural: the thread's own `<todo>` block
11
+ * still has an open item.
12
+ * 2. `classifyStall` — a cheap-model fallback for the harder case: no todo
13
+ * block at all, prose just trails off. Deliberately conservative — the
14
+ * caller must default to "no nudge" on anything it isn't sure about,
15
+ * since prodding a thread that finished and is waiting on the user is
16
+ * the worse failure (Karl, 2026-08-30).
17
+ */
18
+ export declare function hasUncheckedTodo(text: string): boolean;
19
+ /** Last `n` sentences of a message — the classifier only needs the tail, not the whole thing. */
20
+ export declare function lastSentences(text: string, n?: number): string;
21
+ /** The single dangling sentence to quote back in the nudge message. */
22
+ export declare function extractDanglingSentence(text: string): string;
23
+ export interface ClassifyStallOptions {
24
+ timeoutMs?: number;
25
+ /** Claude CLI sub-model id (e.g. "haiku"). Defaults to "haiku". */
26
+ model?: string;
27
+ }
28
+ /**
29
+ * Cheap-model classification for the "trails off with no `<todo>` at all" case.
30
+ *
31
+ * A one-off `claude --print --model <model>` call, not a full agentic turn. Fails
32
+ * closed: any error, non-zero exit, timeout, or non-"YES" response resolves to
33
+ * `false` (no nudge).
34
+ *
35
+ * The prompt goes in as PLAIN TEXT on stdin — not `--input-format stream-json`.
36
+ * That flag requires a matching `--output-format stream-json --verbose`, and without
37
+ * it the CLI errors before reaching the model (`Error: --input-format=stream-json
38
+ * requires output-format=stream-json.`), so the classifier would fail closed on every
39
+ * call and the fallback signal would be dead. Plain text on stdin is the form that
40
+ * actually works and sidesteps ARG_MAX for long prompts.
41
+ */
42
+ export declare function classifyStall(text: string, opts?: ClassifyStallOptions): Promise<boolean>;
43
+ /** Is the last message from the thread's own `<todo>`/classifier signals a stall? */
44
+ export declare function isStalled(text: string, opts?: ClassifyStallOptions): Promise<boolean>;
45
+ /**
46
+ * Resolve the configured stall-classifier model id to a Claude CLI sub-model.
47
+ *
48
+ * The classifier is a one-off `claude --print --model <id>` call, so it can only run
49
+ * on a Claude CLI sub-model — an id from `models[]` (a direct-provider model) has no
50
+ * meaning here. An id that names nothing in `claudeModels[]` (absent, blank, or stale
51
+ * after a catalog edit) falls back to "haiku" rather than failing closed, since the
52
+ * classifier is a best-effort fallback signal, not a correctness gate.
53
+ */
54
+ export declare function resolveStallClassifierModel(stallClassifierModel: string | undefined, claudeModels?: Array<{
55
+ id: string;
56
+ }>): string;
57
+ //# sourceMappingURL=stall-detection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stall-detection.d.ts","sourceRoot":"","sources":["../../src/gateway/stall-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,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;AAaD,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,OAAO,CAAC,CA6DlB;AAED,qFAAqF;AACrF,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,OAAO,CAAC,OAAO,CAAC,CAG/F;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,oBAAoB,EAAE,MAAM,GAAG,SAAS,EACxC,YAAY,CAAC,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,GACnC,MAAM,CAIR"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Stall detection (task 164).
3
+ *
4
+ * A thread that ends a turn on a promised-but-untaken next action ("Let me
5
+ * look at how X renders.") with nothing after it is not empty (task 150/156
6
+ * doesn't catch it) and may never have armed a `schedule_trigger` (task 156
7
+ * only helps if the model remembers to). This module supplies the two
8
+ * detection signals `server.ts` uses on the busy→idle edge:
9
+ *
10
+ * 1. `hasUncheckedTodo` — free, structural: the thread's own `<todo>` block
11
+ * still has an open item.
12
+ * 2. `classifyStall` — a cheap-model fallback for the harder case: no todo
13
+ * block at all, prose just trails off. Deliberately conservative — the
14
+ * caller must default to "no nudge" on anything it isn't sure about,
15
+ * since prodding a thread that finished and is waiting on the user is
16
+ * the worse failure (Karl, 2026-08-30).
17
+ */
18
+ import { spawn } from 'child_process';
19
+ /** Last (authoritative, full-replacement) `<todo>` block still has an open `[ ]` item. */
20
+ const TODO_BLOCK_RE = /<todo>([\s\S]*?)<\/todo>/g;
21
+ const UNCHECKED_ITEM_RE = /\[\s\]/;
22
+ export function hasUncheckedTodo(text) {
23
+ if (!text)
24
+ return false;
25
+ let lastBlock;
26
+ let match;
27
+ TODO_BLOCK_RE.lastIndex = 0;
28
+ while ((match = TODO_BLOCK_RE.exec(text))) {
29
+ lastBlock = match[1];
30
+ }
31
+ if (!lastBlock)
32
+ return false;
33
+ return UNCHECKED_ITEM_RE.test(lastBlock);
34
+ }
35
+ /**
36
+ * Rough sentence split — good enough to isolate the tail of a message.
37
+ *
38
+ * `!` and `?` always end a sentence; `.` only does when it's the end of the
39
+ * text or followed by whitespace and an uppercase/quote/paren — otherwise a
40
+ * mid-word period (`inspect.ts`, `e.g.`) would fracture the tail into
41
+ * fragments and hand the classifier "ts renders claim rows." instead of the
42
+ * whole dangling sentence.
43
+ */
44
+ function splitSentences(text) {
45
+ const trimmed = text.trim();
46
+ if (!trimmed)
47
+ return [];
48
+ const sentences = [];
49
+ let start = 0;
50
+ for (let i = 0; i < trimmed.length; i++) {
51
+ const ch = trimmed[i];
52
+ if (ch === '!' || ch === '?') {
53
+ sentences.push(trimmed.slice(start, i + 1).trim());
54
+ start = i + 1;
55
+ }
56
+ else if (ch === '.') {
57
+ const rest = trimmed.slice(i + 1);
58
+ const boundary = rest.match(/^\s+(\S)/);
59
+ if (!rest || (boundary && /[A-Z"'(]/.test(boundary[1]))) {
60
+ sentences.push(trimmed.slice(start, i + 1).trim());
61
+ start = i + 1;
62
+ }
63
+ }
64
+ }
65
+ if (start < trimmed.length)
66
+ sentences.push(trimmed.slice(start).trim());
67
+ return sentences.filter(Boolean);
68
+ }
69
+ /** Last `n` sentences of a message — the classifier only needs the tail, not the whole thing. */
70
+ export function lastSentences(text, n = 6) {
71
+ return splitSentences(text).slice(-n).join(' ').trim();
72
+ }
73
+ /** The single dangling sentence to quote back in the nudge message. */
74
+ export function extractDanglingSentence(text) {
75
+ const sentences = splitSentences(text);
76
+ return sentences.length > 0 ? sentences[sentences.length - 1] : text.trim();
77
+ }
78
+ const STALL_CLASSIFIER_PROMPT = (snippet) => `You are checking whether an AI assistant's message ends mid-task — having promised an ` +
79
+ `immediate next action it never took — versus ending at a normal stopping point.\n\n` +
80
+ `Message (last portion):\n"""\n${snippet}\n"""\n\n` +
81
+ `Question: Does this message end with an explicit, concrete statement of an immediate next ` +
82
+ `action (e.g. "Let me check X", "I'll look at Y now", "Next I'll do Z") that the message does ` +
83
+ `NOT go on to actually take? Answer YES only if this is unambiguous and obvious. If the ` +
84
+ `message reads as a completed thought, a finished answer, a question back to the user, an ` +
85
+ `offer awaiting the user's decision, or anything you are not certain about, answer NO.\n\n` +
86
+ `Respond with exactly one word: YES or NO.`;
87
+ /**
88
+ * Cheap-model classification for the "trails off with no `<todo>` at all" case.
89
+ *
90
+ * A one-off `claude --print --model <model>` call, not a full agentic turn. Fails
91
+ * closed: any error, non-zero exit, timeout, or non-"YES" response resolves to
92
+ * `false` (no nudge).
93
+ *
94
+ * The prompt goes in as PLAIN TEXT on stdin — not `--input-format stream-json`.
95
+ * That flag requires a matching `--output-format stream-json --verbose`, and without
96
+ * it the CLI errors before reaching the model (`Error: --input-format=stream-json
97
+ * requires output-format=stream-json.`), so the classifier would fail closed on every
98
+ * call and the fallback signal would be dead. Plain text on stdin is the form that
99
+ * actually works and sidesteps ARG_MAX for long prompts.
100
+ */
101
+ export async function classifyStall(text, opts = {}) {
102
+ const snippet = lastSentences(text, 6);
103
+ if (!snippet)
104
+ return false;
105
+ const timeoutMs = opts.timeoutMs ?? 10000;
106
+ const model = opts.model?.trim() || 'haiku';
107
+ return new Promise(resolve => {
108
+ const args = ['--print', '--model', model];
109
+ const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('CLAUDE') && key !== 'CLAUDECODE'));
110
+ let claude;
111
+ try {
112
+ claude = spawn('claude', args, {
113
+ stdio: ['pipe', 'pipe', 'pipe'],
114
+ env: cleanEnv,
115
+ shell: process.platform === 'win32',
116
+ windowsHide: true,
117
+ });
118
+ }
119
+ catch {
120
+ resolve(false);
121
+ return;
122
+ }
123
+ // Plain text on stdin (see doc comment) — no JSON envelope, no --input-format.
124
+ claude.stdin?.write(STALL_CLASSIFIER_PROMPT(snippet));
125
+ claude.stdin?.end();
126
+ let stdout = '';
127
+ let settled = false;
128
+ const finish = (result) => {
129
+ if (settled)
130
+ return;
131
+ settled = true;
132
+ clearTimeout(timer);
133
+ resolve(result);
134
+ };
135
+ const timer = setTimeout(() => {
136
+ claude.kill();
137
+ finish(false);
138
+ }, timeoutMs);
139
+ timer.unref?.();
140
+ claude.stdout?.on('data', (data) => {
141
+ stdout += data.toString();
142
+ });
143
+ claude.on('close', code => {
144
+ if (code !== 0) {
145
+ finish(false);
146
+ return;
147
+ }
148
+ finish(/^YES\b/i.test(stdout.trim()));
149
+ });
150
+ claude.on('error', () => finish(false));
151
+ });
152
+ }
153
+ /** Is the last message from the thread's own `<todo>`/classifier signals a stall? */
154
+ export async function isStalled(text, opts = {}) {
155
+ if (hasUncheckedTodo(text))
156
+ return true;
157
+ return classifyStall(text, opts);
158
+ }
159
+ /**
160
+ * Resolve the configured stall-classifier model id to a Claude CLI sub-model.
161
+ *
162
+ * The classifier is a one-off `claude --print --model <id>` call, so it can only run
163
+ * on a Claude CLI sub-model — an id from `models[]` (a direct-provider model) has no
164
+ * meaning here. An id that names nothing in `claudeModels[]` (absent, blank, or stale
165
+ * after a catalog edit) falls back to "haiku" rather than failing closed, since the
166
+ * classifier is a best-effort fallback signal, not a correctness gate.
167
+ */
168
+ export function resolveStallClassifierModel(stallClassifierModel, claudeModels) {
169
+ const id = stallClassifierModel?.trim();
170
+ if (id && claudeModels?.some(entry => entry.id === id))
171
+ return id;
172
+ return 'haiku';
173
+ }
174
+ //# sourceMappingURL=stall-detection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stall-detection.js","sourceRoot":"","sources":["../../src/gateway/stall-detection.ts"],"names":[],"mappings":"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"}
@@ -139,6 +139,11 @@
139
139
  // and re-adding it does not quietly clear the voice setting. The gateway's
140
140
  // resolveVoiceModel treats an unmatched id as "no override" anyway.
141
141
  voiceModel: typeof config.voiceModel === 'string' ? config.voiceModel : '',
142
+ // Task 164. Same verbatim rule as voiceModel: an id naming nothing in claudeModels[]
143
+ // is preserved on round-trip; the gateway's resolveStallClassifierModel falls back to
144
+ // "haiku" for an unmatched id anyway.
145
+ stallClassifierModel:
146
+ typeof config.stallClassifierModel === 'string' ? config.stallClassifierModel : '',
142
147
  };
143
148
  }
144
149
 
@@ -200,6 +205,9 @@
200
205
  // Task 152: '' means "same as thread" and must reach the server as an explicit
201
206
  // null (clear), not as absent — absent would leave the stored value untouched.
202
207
  voiceModel: state.voiceModel ? state.voiceModel : null,
208
+ // Task 164: '' means "haiku" and must reach the server as an explicit null (clear),
209
+ // not as absent — absent would leave a previously-set classifier model untouched.
210
+ stallClassifierModel: state.stallClassifierModel ? state.stallClassifierModel : null,
203
211
  // Registry travels without credentials — addCredentialPatch fills those
204
212
  // in, so an untouched key round-trips as absent and the server preserves it.
205
213
  customProviders: (Array.isArray(state.customProviders) ? state.customProviders : []).map(
@@ -6461,6 +6469,24 @@
6461
6469
  voiceCard.appendChild(voiceSelect);
6462
6470
  body.appendChild(voiceCard);
6463
6471
 
6472
+ // Task 164: the stall-detection classifier is a one-off `claude --print --model <id>`
6473
+ // call, so it can only run on a Claude CLI sub-model — this dropdown lists ONLY the
6474
+ // Anthropic sub-models (not the full catalog), with "Haiku (default)" clearing the
6475
+ // setting. Same card shape as the voice model above.
6476
+ var stallCard = document.createElement('div');
6477
+ stallCard.className = 'cumulus-settings-default-card';
6478
+ var stallCardLabel = document.createElement('label');
6479
+ stallCardLabel.textContent = 'Stall-detection classifier uses';
6480
+ var stallSelect = document.createElement('select');
6481
+ stallSelect.className = 'cumulus-settings-select';
6482
+ stallSelect.setAttribute('data-testid', 'webchat-gw-stall-classifier-select');
6483
+ stallSelect.addEventListener('change', function () {
6484
+ gwState.stallClassifierModel = stallSelect.value;
6485
+ });
6486
+ stallCard.appendChild(stallCardLabel);
6487
+ stallCard.appendChild(stallSelect);
6488
+ body.appendChild(stallCard);
6489
+
6464
6490
  var modelsLabel = document.createElement('div');
6465
6491
  modelsLabel.className = 'cumulus-settings-section-label';
6466
6492
  modelsLabel.textContent = 'Model catalog';
@@ -7084,6 +7110,29 @@
7084
7110
  if (voiceSelect.value !== (gwState.voiceModel || '')) voiceSelect.value = '';
7085
7111
  }
7086
7112
 
7113
+ // Task 164: the classifier only runs on Claude CLI sub-models, so this lists ONLY the
7114
+ // Anthropic sub-models (not the full catalog) plus a "Haiku (default)" clear option. A
7115
+ // stored id naming nothing in claudeModels[] falls back to "Haiku (default)" in the UI —
7116
+ // which is what the gateway does with it too (resolveStallClassifierModel → "haiku").
7117
+ function renderStallSelect() {
7118
+ stallSelect.innerHTML = '';
7119
+ var haiku = document.createElement('option');
7120
+ haiku.value = '';
7121
+ haiku.textContent = 'Haiku (default)';
7122
+ stallSelect.appendChild(haiku);
7123
+ var anthropic = gwState.catalog.filter(function (model) {
7124
+ return model.provider === 'anthropic';
7125
+ });
7126
+ anthropic.forEach(function (model) {
7127
+ var opt = document.createElement('option');
7128
+ opt.value = model.id;
7129
+ opt.textContent = model.label || model.id;
7130
+ stallSelect.appendChild(opt);
7131
+ });
7132
+ stallSelect.value = gwState.stallClassifierModel || '';
7133
+ if (stallSelect.value !== (gwState.stallClassifierModel || '')) stallSelect.value = '';
7134
+ }
7135
+
7087
7136
  // Rebuild the provider list from state before anything reads it, so a
7088
7137
  // provider added or removed this session reaches the cards, the default
7089
7138
  // dropdown and the add-model select in the same pass (task 147).
@@ -7107,6 +7156,7 @@
7107
7156
  renderAddProviderOptions();
7108
7157
  renderDefaultSelect();
7109
7158
  renderVoiceSelect();
7159
+ renderStallSelect();
7110
7160
  renderGwModels();
7111
7161
  }
7112
7162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckydraw/cumulus",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "RLM-based CLI chat wrapper for Claude with external history context management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",