@nitra/cursor 12.3.1 → 12.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [12.3.2] - 2026-06-20
4
+
5
+ ### Fixed
6
+
7
+ - fix-каскад: per-tier timeout (локалі fail-fast ~45s замість стіни 120s, env N_LOCAL_FIX_TIMEOUT_MS/N_CLOUD_FIX_TIMEOUT_MS) + хмарний транспортний збій (pi ETIMEDOUT/spawn) обриває драбину замість ескалації на cloud-avg — не палиться avg-бюджет
8
+
3
9
  ## [12.3.1] - 2026-06-20
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "12.3.1",
3
+ "version": "12.3.2",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: llm-worker.mjs
4
4
  resource: npm/scripts/lib/fix/llm-worker.mjs
5
5
  docgen:
6
- crc: de9eb68c
6
+ crc: 857c510e
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -18,7 +18,7 @@ docgen:
18
18
 
19
19
  ## Публічний API
20
20
 
21
- - `runLlmWorker(ruleId, violationOutput, projectRoot, opts)` — виправляє одне порушення; `opts`: `model`, `feedback`, `caller`. Повертає `{ ok, error, changes, diagnosis }`.
21
+ - `runLlmWorker(ruleId, violationOutput, projectRoot, opts)` — виправляє одне порушення; `opts`: `model`, `feedback`, `caller`, `timeoutMs` (per-tier ліміт виклику; драбина задає коротший для локальних рунгів). Повертає `{ ok, error, changes, diagnosis }`.
22
22
 
23
23
  ## Гарантії поведінки
24
24
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: orchestrator.mjs
4
4
  resource: npm/scripts/lib/fix/orchestrator.mjs
5
5
  docgen:
6
- crc: 78dfe86b
6
+ crc: d327ab6d
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -19,7 +19,8 @@ docgen:
19
19
  - рунг `local-min` — перший прохід без feedback;
20
20
  - рунг `local-min-retry` — той самий локальний тир, але з feedback попереднього рунга (попередні зміни + залишковий violation);
21
21
  - рунги `cloud-min` / `cloud-avg` — хмарні моделі (через pi), теж із feedback.
22
- 5. Достроковий вихід драбини: systemic-помилка локального тиру пропускає рунги тієї ж моделі; відсутній API-ключ на хмарному обриває драбину; вичерпаний avg-кеп пропускає avg-рунг (із записом у лог).
22
+ Кожен рунг має per-tier `timeoutMs`: локальні **fail-fast** (`N_LOCAL_FIX_TIMEOUT_MS`, дефолт 45s не палити стіну 120s на повільному локальному inference), хмарні повний (`N_CLOUD_FIX_TIMEOUT_MS`, дефолт 120s).
23
+ 5. Достроковий вихід драбини: systemic-помилка локального тиру пропускає рунги тієї ж моделі; відсутній API-ключ на хмарному обриває драбину; хмарний транспортний збій (pi таймаут/spawn) обриває драбину, щоб не палити avg-бюджет на ту саму стіну; вичерпаний avg-кеп пропускає avg-рунг (із записом у лог).
23
24
  6. Після обробки всіх правил — фінальна перевірка. Усі чисті → успіх; інакше — ознака нерозв'язаних.
24
25
 
25
26
  ## Публічний API
@@ -117,11 +117,12 @@ function buildPrompt(ruleId, ruleMdc, output, files, feedback = null) {
117
117
  * @param {string} prompt текст промпта
118
118
  * @param {string} model назва моделі (provider/id, `omlx/...` або '')
119
119
  * @param {string} caller мітка викликача для wire-trace (`fix:<rule>:<rung>`)
120
+ * @param {number} [timeoutMs] ліміт виклику (драбина задає per-tier; undefined → дефолт callLlm)
120
121
  * @returns {{ text: string, error?: string }} текст відповіді або повідомлення про помилку
121
122
  */
122
- function callModel(prompt, model, caller) {
123
+ function callModel(prompt, model, caller, timeoutMs) {
123
124
  try {
124
- return { text: callLlm([{ role: 'user', content: prompt }], model, { timeoutMs: 120_000, caller }) }
125
+ return { text: callLlm([{ role: 'user', content: prompt }], model, { timeoutMs, caller }) }
125
126
  } catch (error) {
126
127
  const msg = String(error.message)
127
128
  if (API_KEY_RE.test(msg)) {
@@ -146,9 +147,10 @@ function callModel(prompt, model, caller) {
146
147
  * @param {string} ruleId ID правила
147
148
  * @param {string} violationOutput output з fix check для цього rule
148
149
  * @param {string} projectRoot абсолютний шлях до кореня проєкту
149
- * @param {{ model?: string, feedback?: object|null, caller?: string }} opts опції:
150
+ * @param {{ model?: string, feedback?: object|null, caller?: string, timeoutMs?: number }} opts опції:
150
151
  * `model` — перевизначення моделі; `feedback` — контекст попереднього рунга
151
- * драбини (retry-with-feedback); `caller` — мітка для wire-trace
152
+ * драбини (retry-with-feedback); `caller` — мітка для wire-trace; `timeoutMs` —
153
+ * per-tier ліміт виклику (драбина: локалі fail-fast, хмара повний)
152
154
  * @returns {{ ok: boolean, error?: string, changes: Array<{path:string}>, diagnosis: string|null }}
153
155
  * статус виправлення, помилка, запропоновані зміни і само-аналіз моделі
154
156
  */
@@ -156,6 +158,7 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
156
158
  const model = opts.model ?? MODEL
157
159
  const feedback = opts.feedback ?? null
158
160
  const caller = opts.caller ?? 'fix'
161
+ const timeoutMs = opts.timeoutMs
159
162
 
160
163
  // 1. Читаємо rule .mdc
161
164
  const mdcPath = join(projectRoot, '.cursor', 'rules', `n-${ruleId}.mdc`)
@@ -166,7 +169,7 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
166
169
 
167
170
  // 3. Будуємо prompt і викликаємо модель
168
171
  const prompt = buildPrompt(ruleId, ruleMdc, violationOutput, files, feedback)
169
- const { text, error: modelError } = callModel(prompt, model, caller)
172
+ const { text, error: modelError } = callModel(prompt, model, caller, timeoutMs)
170
173
 
171
174
  if (modelError) return { ok: false, error: modelError, changes: [], diagnosis: null }
172
175
  if (!text) return { ok: false, error: 'model returned empty response', changes: [], diagnosis: null }
@@ -1,5 +1,6 @@
1
1
  /** @see ./docs/orchestrator.md */
2
2
 
3
+ import { env } from 'node:process'
3
4
  import { runFixCheck } from './run-fix-check.mjs'
4
5
  import { runT0AutoCli } from './t0.mjs'
5
6
  import { logEscalation } from './escalation-log.mjs'
@@ -13,9 +14,24 @@ import { CLOUD_AVG, CLOUD_MIN, LOCAL_MIN } from '../../../lib/models.mjs'
13
14
  */
14
15
  const DEFAULT_MAX_AVG = 3
15
16
 
17
+ /**
18
+ * Timeout одного LLM-виклику за тиром. Локальні рунги **fail-fast**: не палити
19
+ * стіну 120s на повільному 4b (curl exit 28) — швидше абортнути й ескалувати.
20
+ * Хмарні — повний. Перевизначення: `N_LOCAL_FIX_TIMEOUT_MS` / `N_CLOUD_FIX_TIMEOUT_MS`.
21
+ */
22
+ const LOCAL_TIMEOUT_MS = Number(env.N_LOCAL_FIX_TIMEOUT_MS) || 45_000
23
+ const CLOUD_TIMEOUT_MS = Number(env.N_CLOUD_FIX_TIMEOUT_MS) || 120_000
24
+
16
25
  /** Маркер дружнього повідомлення про відсутній API-ключ (з `llm-worker.callModel`). */
17
26
  const NO_KEY_RE = /немає ключа|api key/i
18
27
 
28
+ /**
29
+ * Хмарний транспорт (pi) упав на рівні процесу: таймаут/spawn-помилка. Стіна часу
30
+ * однакова для всіх cloud-рунгів (та сама pi-транспортна стіна), а cloud-avg — інша
31
+ * модель, не більший timeout. Ескалація на неї лише спалить avg-бюджет → обрив.
32
+ */
33
+ const CLOUD_TRANSPORT_RE = /etimedout|timed out|pi error/i
34
+
19
35
  /**
20
36
  * Будує драбину ескалації за наявними тирами (спека 2026-06-19-fix-escalation-cascade):
21
37
  * 1. `local-min` — `N_LOCAL_MIN_MODEL`, перший прохід;
@@ -24,14 +40,14 @@ const NO_KEY_RE = /немає ключа|api key/i
24
40
  * 4. `cloud-avg` — `N_CLOUD_AVG_MODEL` (через pi), з feedback, під avg-кепом.
25
41
  * Рунги з незаданим тиром (`''`) відсіюються — драбина стискається до доступних.
26
42
  * @param {{ localMin: string, cloudMin: string, cloudAvg: string }} models тири з env
27
- * @returns {Array<{ tier: string, model: string, feedback: boolean, local: boolean, isAvg: boolean }>} драбина
43
+ * @returns {Array<{ tier: string, model: string, feedback: boolean, local: boolean, isAvg: boolean, timeoutMs: number }>} драбина
28
44
  */
29
45
  export function buildLadder({ localMin, cloudMin, cloudAvg }) {
30
46
  return [
31
- { tier: 'local-min', model: localMin, feedback: false, local: true, isAvg: false },
32
- { tier: 'local-min-retry', model: localMin, feedback: true, local: true, isAvg: false },
33
- { tier: 'cloud-min', model: cloudMin, feedback: true, local: false, isAvg: false },
34
- { tier: 'cloud-avg', model: cloudAvg, feedback: true, local: false, isAvg: true }
47
+ { tier: 'local-min', model: localMin, feedback: false, local: true, isAvg: false, timeoutMs: LOCAL_TIMEOUT_MS },
48
+ { tier: 'local-min-retry', model: localMin, feedback: true, local: true, isAvg: false, timeoutMs: LOCAL_TIMEOUT_MS },
49
+ { tier: 'cloud-min', model: cloudMin, feedback: true, local: false, isAvg: false, timeoutMs: CLOUD_TIMEOUT_MS },
50
+ { tier: 'cloud-avg', model: cloudAvg, feedback: true, local: false, isAvg: true, timeoutMs: CLOUD_TIMEOUT_MS }
35
51
  ].filter(r => r.model)
36
52
  }
37
53
 
@@ -40,6 +56,8 @@ export function buildLadder({ localMin, cloudMin, cloudAvg }) {
40
56
  * - `break` — відсутній API-ключ на хмарному (інші хмарні рунги теж без ключа);
41
57
  * - `skip-model` — systemic-помилка локального тиру (memory-guard/auth/down): повтор
42
58
  * тієї ж моделі марний → пропустити рунги з цим model.
59
+ * - `break` — також хмарний транспорт упав (pi таймаут/spawn): решта cloud-рунгів
60
+ * під тією ж стіною → обрив, щоб не палити avg-бюджет.
43
61
  * @param {{ local: boolean }} rung поточний рунг
44
62
  * @param {string|null|undefined} error помилка виклику worker
45
63
  * @returns {'break'|'skip-model'|null} дія для драбини
@@ -48,6 +66,7 @@ function decideAfterFailure(rung, error) {
48
66
  if (!error) return null
49
67
  if (NO_KEY_RE.test(error)) return 'break'
50
68
  if (rung.local && classifyOmlxError(error) === 'systemic') return 'skip-model'
69
+ if (!rung.local && CLOUD_TRANSPORT_RE.test(error)) return 'break'
51
70
  return null
52
71
  }
53
72
 
@@ -93,7 +112,8 @@ export async function escalateRule(rule, cwd, deps) {
93
112
  const res = worker.runLlmWorker(rule.ruleId, currentViolation, cwd, {
94
113
  model: rung.model,
95
114
  feedback: rung.feedback ? feedback : null,
96
- caller: `fix:${rule.ruleId}:${rung.tier}`
115
+ caller: `fix:${rule.ruleId}:${rung.tier}`,
116
+ timeoutMs: rung.timeoutMs
97
117
  })
98
118
  if (rung.isAvg) avgUsed++
99
119