micro-models-agent 2.11.0 → 2.13.1

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 (356) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/assets/app.js +22 -2
  3. package/dist/assets/index.html +39 -9
  4. package/dist/assets/js/dom.js +15 -0
  5. package/dist/assets/js/feed.js +129 -10
  6. package/dist/assets/js/sidebar.js +512 -0
  7. package/dist/assets/js/tabs.js +30 -5
  8. package/dist/assets/setup.js +267 -8
  9. package/dist/assets/styles.css +168 -3
  10. package/dist/cli/cache-line.js +1 -0
  11. package/dist/cli/command-suggest.js +5 -4
  12. package/dist/cli/commands.js +231 -122
  13. package/dist/cli/completer.js +132 -31
  14. package/dist/cli/index.js +1 -0
  15. package/dist/cli/json-payload.js +4 -4
  16. package/dist/cli/main.js +166 -61
  17. package/dist/cli/onboarding-commands.js +126 -0
  18. package/dist/cli/plugin-commands.js +2 -1
  19. package/dist/cli/relaunch.js +16 -13
  20. package/dist/cli/repl-commands-lsp.js +75 -0
  21. package/dist/cli/repl-commands-skills.js +105 -0
  22. package/dist/cli/repl-commands.js +589 -266
  23. package/dist/cli/repl.js +436 -177
  24. package/dist/cli/run-result.js +5 -6
  25. package/dist/cli/security-commands.js +37 -28
  26. package/dist/cli/setup-order.js +4 -55
  27. package/dist/cli/setup-prompt.js +3 -2
  28. package/dist/cli/setup.js +27 -107
  29. package/dist/cli/web-command.js +98 -0
  30. package/dist/config/budget.js +5 -5
  31. package/dist/config/config.js +103 -96
  32. package/dist/config/defaults.js +38 -8
  33. package/dist/config/domains.js +24 -27
  34. package/dist/config/env.js +46 -0
  35. package/dist/config/experts.js +2 -0
  36. package/dist/config/index.js +1 -0
  37. package/dist/config/paths.js +56 -0
  38. package/dist/config/redact.js +44 -0
  39. package/dist/config/registry-extra.js +334 -0
  40. package/dist/config/registry.js +584 -0
  41. package/dist/config/security.js +35 -36
  42. package/dist/config/timeouts.js +74 -0
  43. package/dist/config/utils.js +16 -15
  44. package/dist/core/agent/artifact-progress.js +59 -0
  45. package/dist/core/agent/artifact-resolve.js +79 -0
  46. package/dist/core/agent/audit-gate.js +7 -3
  47. package/dist/core/agent/compaction.js +2 -2
  48. package/dist/core/agent/constants.js +33 -4
  49. package/dist/core/agent/context-log.js +454 -0
  50. package/dist/core/agent/context-renderer.js +5 -0
  51. package/dist/core/agent/critic-gate.js +132 -0
  52. package/dist/core/agent/hallucination-gate.js +148 -38
  53. package/dist/core/agent/loop-state.js +15 -0
  54. package/dist/core/agent/prefix-monitor.js +1 -1
  55. package/dist/core/agent/reasoning-resolver.js +10 -1
  56. package/dist/core/agent/token-tracker.js +7 -4
  57. package/dist/core/agent/tool-batch.js +93 -18
  58. package/dist/core/agent/tool-images.js +18 -0
  59. package/dist/core/agent/tool-output.js +65 -11
  60. package/dist/core/agent/tool-pairing.js +60 -0
  61. package/dist/core/agent-moe.js +25 -24
  62. package/dist/core/agent.js +429 -108
  63. package/dist/core/bootstrap/agent-phase.js +66 -0
  64. package/dist/core/bootstrap/config-phase.js +86 -0
  65. package/dist/core/bootstrap/modules-phase.js +269 -0
  66. package/dist/core/bootstrap/provider-phase.js +183 -0
  67. package/dist/core/bootstrap/session-phase.js +204 -0
  68. package/dist/core/bootstrap/state.js +94 -0
  69. package/dist/core/bootstrap/workspace-phase.js +115 -0
  70. package/dist/core/bootstrap.js +92 -660
  71. package/dist/core/crash-handler.js +17 -7
  72. package/dist/core/environment.js +37 -12
  73. package/dist/core/host-plan.js +43 -0
  74. package/dist/core/host.js +23 -0
  75. package/dist/core/index.js +1 -0
  76. package/dist/core/prompt-builder.js +17 -1
  77. package/dist/core/prompt-overflow.js +42 -43
  78. package/dist/core/session-logger.js +122 -30
  79. package/dist/core/system-prompt.js +117 -0
  80. package/dist/core/version.js +6 -4
  81. package/dist/core/web-host.js +34 -0
  82. package/dist/core/workspace.js +19 -15
  83. package/dist/i18n/en.json +89 -3
  84. package/dist/i18n/index.js +37 -3
  85. package/dist/i18n/ru.json +89 -3
  86. package/dist/index.js +7 -3
  87. package/dist/llm/image-utils.js +55 -64
  88. package/dist/llm/llm-errors.js +9 -0
  89. package/dist/llm/model-loader.js +10 -32
  90. package/dist/llm/models-dev.js +121 -0
  91. package/dist/llm/openai-compat.js +146 -73
  92. package/dist/llm/orchestrator.js +33 -11
  93. package/dist/llm/provider-budget.js +2 -2
  94. package/dist/llm/provider-context.js +233 -0
  95. package/dist/llm/provider.js +1 -1
  96. package/dist/llm/response.js +5 -0
  97. package/dist/llm/token-counter.js +27 -11
  98. package/dist/logger/app-logger.js +48 -22
  99. package/dist/logger/file-log.js +28 -19
  100. package/dist/logger/index.js +1 -0
  101. package/dist/main.js +888 -139
  102. package/dist/migration/backup.js +14 -3
  103. package/dist/migration/detect.js +12 -3
  104. package/dist/migration/index.js +1 -0
  105. package/dist/modules/artifacts/store.js +9 -8
  106. package/dist/modules/browser/actions.js +6 -0
  107. package/dist/modules/browser/bridge-client.js +14 -12
  108. package/dist/modules/browser/bridge-path.js +4 -4
  109. package/dist/modules/browser/console-buffer.js +37 -0
  110. package/dist/modules/browser/cookie-store.js +5 -0
  111. package/dist/modules/browser/driver-types.js +1 -0
  112. package/dist/modules/browser/driver.js +19 -11
  113. package/dist/modules/browser/index.js +3 -1
  114. package/dist/modules/browser/module.js +2 -1
  115. package/dist/modules/browser/session.js +10 -30
  116. package/dist/modules/browser/snapshot.js +19 -13
  117. package/dist/modules/browser/types.js +1 -0
  118. package/dist/modules/certification/cli.js +28 -20
  119. package/dist/modules/certification/fact-checker.js +9 -1
  120. package/dist/modules/certification/loader.js +9 -2
  121. package/dist/modules/certification/manifest.js +83 -21
  122. package/dist/modules/certification/runner.js +23 -13
  123. package/dist/modules/certification/scenarios.js +20 -20
  124. package/dist/modules/certification/syntax-scenarios.js +1 -2
  125. package/dist/modules/context/chunk-query.js +11 -9
  126. package/dist/modules/context/fact-extractor.js +37 -39
  127. package/dist/modules/context/index.js +1 -0
  128. package/dist/modules/context/manager.js +106 -80
  129. package/dist/modules/critic/client.js +176 -0
  130. package/dist/modules/critic/evidence.js +81 -0
  131. package/dist/modules/critic/provider.js +45 -0
  132. package/dist/modules/critic/tools.js +78 -0
  133. package/dist/modules/critic/types.js +1 -0
  134. package/dist/modules/execution/audit-runners.js +28 -34
  135. package/dist/modules/execution/auditor.js +53 -49
  136. package/dist/modules/execution/execution-plugin.js +142 -102
  137. package/dist/modules/execution/index.js +1 -1
  138. package/dist/modules/execution/input-from.js +37 -13
  139. package/dist/modules/execution/module.js +368 -191
  140. package/dist/modules/execution/moe-executor.js +53 -26
  141. package/dist/modules/execution/plan-actions.js +320 -202
  142. package/dist/modules/execution/plan-checks.js +35 -0
  143. package/dist/modules/execution/plan-coverage.js +36 -33
  144. package/dist/modules/execution/plan-steps.js +11 -3
  145. package/dist/modules/execution/plan-store.js +83 -17
  146. package/dist/modules/execution/plan-tool.js +89 -35
  147. package/dist/modules/execution/plan-validator.js +30 -10
  148. package/dist/modules/execution/planner.js +22 -13
  149. package/dist/modules/execution/stuck-detector.js +196 -93
  150. package/dist/modules/execution/tracker.js +22 -8
  151. package/dist/modules/execution/verifier.js +69 -48
  152. package/dist/modules/execution/windows-commands.js +12 -12
  153. package/dist/modules/hallucination/artifact-claim.js +93 -0
  154. package/dist/modules/hallucination/confidence.js +13 -11
  155. package/dist/modules/hallucination/consistency.js +10 -2
  156. package/dist/modules/hallucination/detector.js +10 -5
  157. package/dist/modules/hallucination/factual.js +43 -26
  158. package/dist/modules/hallucination/index.js +1 -0
  159. package/dist/modules/hallucination/js-identifiers.js +44 -45
  160. package/dist/modules/hallucination/llm-judge.js +17 -12
  161. package/dist/modules/hallucination/success-claim.js +59 -0
  162. package/dist/modules/indexer/cache.js +52 -4
  163. package/dist/modules/indexer/index.js +1 -0
  164. package/dist/modules/indexer/map-command.js +2 -4
  165. package/dist/modules/indexer/map-select.js +16 -16
  166. package/dist/modules/indexer/module.js +47 -12
  167. package/dist/modules/indexer/project-profile.js +8 -7
  168. package/dist/modules/indexer/symbols.js +15 -15
  169. package/dist/modules/indexer/walker.js +159 -29
  170. package/dist/modules/lsp/check-tool.js +7 -5
  171. package/dist/modules/lsp/client.js +39 -39
  172. package/dist/modules/lsp/command.js +17 -24
  173. package/dist/modules/lsp/config.js +12 -11
  174. package/dist/modules/lsp/index.js +1 -0
  175. package/dist/modules/lsp/module.js +48 -46
  176. package/dist/modules/lsp/probe.js +13 -14
  177. package/dist/modules/lsp/project-root.js +14 -17
  178. package/dist/modules/lsp/startup-check.js +18 -16
  179. package/dist/modules/mcp/client.js +59 -62
  180. package/dist/modules/mcp/index.js +1 -0
  181. package/dist/modules/mcp/module.js +66 -37
  182. package/dist/modules/mcp/registry.js +5 -0
  183. package/dist/modules/memory/index.js +1 -0
  184. package/dist/modules/memory/module.js +12 -8
  185. package/dist/modules/memory/search.js +4 -1
  186. package/dist/modules/memory/store.js +40 -2
  187. package/dist/modules/onboarding/init.js +47 -0
  188. package/dist/modules/onboarding/meet.js +30 -0
  189. package/dist/modules/onboarding/mode.js +30 -0
  190. package/dist/modules/onboarding/prompts.js +43 -0
  191. package/dist/modules/pipelines/engine.js +10 -0
  192. package/dist/modules/pipelines/index.js +1 -0
  193. package/dist/modules/pipelines/parser.js +4 -1
  194. package/dist/modules/pipelines/template.js +4 -0
  195. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  196. package/dist/modules/plugins/builtin/notify.js +2 -0
  197. package/dist/modules/plugins/index.js +1 -0
  198. package/dist/modules/plugins/loader.js +18 -3
  199. package/dist/modules/plugins/manager.js +37 -17
  200. package/dist/modules/pricing/index.js +10 -11
  201. package/dist/modules/pricing/prices.js +12 -12
  202. package/dist/modules/processes/index.js +1 -0
  203. package/dist/modules/processes/kill-tree.js +27 -31
  204. package/dist/modules/processes/registry.js +44 -25
  205. package/dist/modules/processes/runner.js +4 -4
  206. package/dist/modules/providers/capabilities.js +28 -0
  207. package/dist/modules/providers/create.js +3 -4
  208. package/dist/modules/providers/factory.js +7 -7
  209. package/dist/modules/providers/fallback.js +12 -12
  210. package/dist/modules/providers/health.js +4 -4
  211. package/dist/modules/providers/manager.js +50 -19
  212. package/dist/modules/providers/naming.js +15 -0
  213. package/dist/modules/providers/preflight.js +71 -0
  214. package/dist/modules/providers/presets.js +39 -8
  215. package/dist/modules/providers/registry.js +7 -2
  216. package/dist/modules/reasoning/policy.js +8 -6
  217. package/dist/modules/reasoning/probe.js +30 -25
  218. package/dist/modules/security/audit-log.js +19 -36
  219. package/dist/modules/security/audit-notifier.js +35 -75
  220. package/dist/modules/security/command-validator.js +99 -51
  221. package/dist/modules/security/content-scanner.js +8 -8
  222. package/dist/modules/security/data-sanitizer.js +9 -16
  223. package/dist/modules/security/encryption.js +32 -52
  224. package/dist/modules/security/index.js +2 -1
  225. package/dist/modules/security/network-validator.js +120 -43
  226. package/dist/modules/security/path-validator.js +61 -46
  227. package/dist/modules/security/protected-paths.js +73 -0
  228. package/dist/modules/security/rate-limiter.js +17 -49
  229. package/dist/modules/security/security-policies.js +25 -57
  230. package/dist/modules/security/session-encryption.js +32 -71
  231. package/dist/modules/security/session-isolation.js +15 -28
  232. package/dist/modules/session/auto-name.js +63 -0
  233. package/dist/modules/session/export.js +98 -0
  234. package/dist/modules/session/index.js +1 -0
  235. package/dist/modules/session/manager.js +79 -28
  236. package/dist/modules/session/module.js +4 -3
  237. package/dist/modules/session/store.js +57 -20
  238. package/dist/modules/setup/answers.js +35 -0
  239. package/dist/modules/setup/apply.js +79 -0
  240. package/dist/modules/setup/discovery.js +72 -0
  241. package/dist/modules/skills/index.js +1 -0
  242. package/dist/modules/skills/loader.js +9 -1
  243. package/dist/modules/skills/module.js +11 -0
  244. package/dist/modules/updater/changelog-reader.js +15 -16
  245. package/dist/modules/updater/checker.js +9 -4
  246. package/dist/modules/updater/dev-detect.js +7 -6
  247. package/dist/modules/updater/index.js +1 -0
  248. package/dist/modules/updater/module.js +17 -15
  249. package/dist/modules/user-profile/compressor.js +2 -0
  250. package/dist/modules/user-profile/index.js +1 -0
  251. package/dist/modules/user-profile/profile.js +10 -1
  252. package/dist/modules/webui/answer-api.js +29 -0
  253. package/dist/modules/webui/assets.js +50 -0
  254. package/dist/modules/webui/body.js +21 -0
  255. package/dist/modules/webui/commands-settings.js +209 -0
  256. package/dist/modules/webui/commands-shared.js +60 -0
  257. package/dist/modules/webui/commands.js +174 -0
  258. package/dist/modules/webui/config-api.js +134 -0
  259. package/dist/modules/webui/file-api.js +202 -0
  260. package/dist/modules/webui/forward.js +159 -0
  261. package/dist/modules/webui/history-api.js +112 -0
  262. package/dist/modules/webui/host-state.js +60 -0
  263. package/dist/modules/webui/i18n-api.js +9 -0
  264. package/dist/modules/webui/limits.js +6 -0
  265. package/dist/modules/webui/log-forward.js +20 -0
  266. package/dist/modules/webui/memory-commands.js +64 -0
  267. package/dist/modules/webui/model-cache.js +18 -0
  268. package/dist/modules/webui/module.js +148 -0
  269. package/dist/modules/webui/persist.js +24 -0
  270. package/dist/modules/webui/prompt-io.js +97 -0
  271. package/dist/modules/webui/providers-api.js +337 -0
  272. package/dist/modules/webui/providers-view.js +56 -0
  273. package/dist/modules/webui/reasoning-view.js +15 -0
  274. package/dist/modules/webui/run-agent.js +81 -0
  275. package/dist/modules/webui/server.js +284 -0
  276. package/dist/modules/webui/sessions-api.js +178 -0
  277. package/dist/modules/webui/settings-api.js +561 -0
  278. package/dist/modules/webui/setup-api.js +201 -0
  279. package/dist/modules/webui/state.js +74 -0
  280. package/dist/modules/webui/util.js +261 -0
  281. package/dist/output/bus.js +7 -2
  282. package/dist/output/channel.js +80 -28
  283. package/dist/output/format.js +2 -1
  284. package/dist/output/index.js +1 -0
  285. package/dist/output/json-sink.js +5 -3
  286. package/dist/output/machine.js +4 -3
  287. package/dist/output/session-sink.js +4 -4
  288. package/dist/tools/approve.js +4 -0
  289. package/dist/tools/attach-image.js +5 -4
  290. package/dist/tools/auto-fixer.js +26 -44
  291. package/dist/tools/auto-tags.js +83 -0
  292. package/dist/tools/bash.js +193 -116
  293. package/dist/tools/browser.js +76 -31
  294. package/dist/tools/chunk-query.js +14 -6
  295. package/dist/tools/create-dir.js +9 -5
  296. package/dist/tools/delete-file.js +9 -5
  297. package/dist/tools/download-file.js +32 -7
  298. package/dist/tools/edit-file.js +52 -14
  299. package/dist/tools/enable-tools.js +6 -6
  300. package/dist/tools/executor.js +132 -27
  301. package/dist/tools/file-info.js +3 -1
  302. package/dist/tools/filter-tools.js +3 -3
  303. package/dist/tools/glob-tool.js +4 -1
  304. package/dist/tools/grep-tool.js +13 -10
  305. package/dist/tools/hidden-tools-block.js +6 -6
  306. package/dist/tools/index.js +16 -8
  307. package/dist/tools/list-dir.js +4 -2
  308. package/dist/tools/load-skill.js +7 -0
  309. package/dist/tools/mcp-call.js +15 -4
  310. package/dist/tools/meet-done.js +14 -0
  311. package/dist/tools/move-file.js +14 -7
  312. package/dist/tools/path-utils.js +29 -29
  313. package/dist/tools/pipeline-run.js +29 -7
  314. package/dist/tools/preview.js +1 -1
  315. package/dist/tools/process-ids.js +31 -0
  316. package/dist/tools/process-kill.js +9 -16
  317. package/dist/tools/process-list.js +1 -0
  318. package/dist/tools/process-log.js +11 -16
  319. package/dist/tools/process-wait.js +63 -0
  320. package/dist/tools/prompt-io.js +4 -3
  321. package/dist/tools/question.js +172 -25
  322. package/dist/tools/read-file.js +55 -34
  323. package/dist/tools/recall.js +14 -8
  324. package/dist/tools/registry.js +6 -5
  325. package/dist/tools/remember.js +8 -4
  326. package/dist/tools/scope-request.js +11 -6
  327. package/dist/tools/search-history.js +5 -2
  328. package/dist/tools/session-info.js +2 -0
  329. package/dist/tools/set-thinking.js +8 -4
  330. package/dist/tools/subagent.js +59 -39
  331. package/dist/tools/syntax-validator.js +292 -90
  332. package/dist/tools/user-input.js +22 -18
  333. package/dist/tools/web-browse.js +9 -3
  334. package/dist/tools/web-fetch.js +12 -23
  335. package/dist/tools/web-search.js +7 -5
  336. package/dist/tools/write-file.js +45 -15
  337. package/dist/ui/action-log.js +13 -0
  338. package/dist/ui/box.js +4 -3
  339. package/dist/ui/colors.js +2 -0
  340. package/dist/ui/diff.js +6 -5
  341. package/dist/ui/index.js +1 -0
  342. package/dist/ui/line-editor.js +230 -122
  343. package/dist/ui/line-math.js +10 -2
  344. package/dist/ui/md-formatter.js +57 -37
  345. package/dist/ui/output.js +8 -3
  346. package/dist/ui/plan-view.js +21 -10
  347. package/dist/ui/renderer.js +393 -67
  348. package/dist/ui/spinner.js +16 -5
  349. package/dist/ui/table.js +15 -17
  350. package/dist/ui/theme.js +143 -0
  351. package/dist/utils/child-run.js +20 -0
  352. package/dist/utils/error.js +1 -1
  353. package/dist/utils/index.js +2 -0
  354. package/dist/utils/retry.js +2 -2
  355. package/dist/utils/truncate.js +1 -1
  356. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ // Детектор застревания: повторы, фейлы, read-only петли, bash-флейлинг, хинты.
1
2
  import { t } from "../../i18n/index";
2
3
  import { isTransientError } from "./transient-error";
3
4
  const BLOCKING_PATTERNS = [
@@ -11,6 +12,7 @@ const BLOCKING_PATTERNS = [
11
12
  /401/i,
12
13
  /403/i,
13
14
  ];
15
+ /** Классифицирует вывод ошибки: транзиентная / блокирующая / системная. */
14
16
  export function classifyError(output) {
15
17
  if (isTransientError(output))
16
18
  return "transient";
@@ -21,11 +23,11 @@ export function classifyError(output) {
21
23
  return "systematic";
22
24
  }
23
25
  /**
24
- * Tools that only observe state and never change it. A long run of only these
25
- * with no write/exec in between means the model is reading/exploring without
26
- * producing anything — a read-only loop that the stuck detector must surface
27
- * (observed: 50+ read_file iterations with zero writes after a false-positive
28
- * audit rejected a completed task).
26
+ * Тулы только чтения: состояние наблюдают, не меняют. Длинная серия только их
27
+ * без write/exec между — модель читает/разведывает, ничего не производя:
28
+ * read-only петля, которую детектор обязан подсветить (было: 50+ read_file
29
+ * итераций с нулём записей после ложноположительного реджекта готового
30
+ * аудита задачи).
29
31
  */
30
32
  const READ_ONLY_TOOLS = new Set([
31
33
  "read_file",
@@ -44,19 +46,19 @@ const READ_ONLY_TOOLS = new Set([
44
46
  "process_list",
45
47
  "process_log",
46
48
  "plan",
49
+ "plan_step",
47
50
  "todo",
48
51
  "verify",
49
52
  "load_skill",
50
53
  ]);
51
- /** Consecutive read-only tool calls that constitute a loop. */
54
+ /** Подряд read-only вызовов, образующих петлю. */
52
55
  const READ_ONLY_LOOP_THRESHOLD = 10;
53
56
  /**
54
- * Bash flailing: ≥BASH_FLAIL_THRESHOLD failures within the last
55
- * BASH_FLAIL_WINDOW bash attempts, regardless of interleaved successes.
56
- * The per-word hard-stop (bash.ts) is evaded by rotating commands
57
- * (wc → cat → Get-Content → …); this window catches the rotation itself
58
- * (observed in ses_mt4x3zbo: 8 failed shell tricks across ~12 iterations
59
- * trying to count/merge files on Windows).
57
+ * Bash-флейлинг: ≥BASH_FLAIL_THRESHOLD фейлов в последних BASH_FLAIL_WINDOW
58
+ * bash-попытках, независимо от перемежающихся успехов. Per-word hard-stop
59
+ * (bash.ts) обходится ротацией команд (wc → cat → Get-Content → …); окно
60
+ * ловит саму ротацию (было в ses_mt4x3zbo: 8 фейловых shell-уловок за ~12
61
+ * итераций при подсчёте/склейке файлов на Windows).
60
62
  */
61
63
  const BASH_FLAIL_WINDOW = 6;
62
64
  const BASH_FLAIL_THRESHOLD = 4;
@@ -112,10 +114,10 @@ const FRAMEWORK_WORDS = [
112
114
  "tailwind",
113
115
  ];
114
116
  /**
115
- * Stable identity for an error: tool + normalized first error lines (ANSI
116
- * stripped, paths/locations replaced, whitespace collapsed, capped at 120
117
- * chars). Two runs failing with the same root cause produce the same
118
- * signature — that is what triggers the automatic web search.
117
+ * Стабильная идентичность ошибки: тул + нормализованные первые error-строки
118
+ * (ANSI вычищен, пути/локации заменены, пробелы склеены, кап 120 символов).
119
+ * Два прогона с одной первопричиной дают одну сигнатуру — она и триггерит
120
+ * автопоиск в вебе.
119
121
  */
120
122
  export function normalizeErrorSignature(toolName, output) {
121
123
  if (!output)
@@ -134,21 +136,20 @@ export function normalizeErrorSignature(toolName, output) {
134
136
  return `${toolName}::${cleaned.slice(0, SIGNATURE_MAX_CHARS)}`;
135
137
  }
136
138
  /**
137
- * Gate for the automatic web search: only real, meaningful error text is
138
- * worth searching for. "Command failed with exit code 1" without details is
139
- * noise — searching it would just burn a query.
139
+ * Гейт автопоиска в вебе: искать стоит только реальный содержательный текст
140
+ * ошибки. "Command failed with exit code 1" без деталей — шум, поиск по нему
141
+ * лишь жжёт запрос.
140
142
  */
141
143
  export function isSearchableError(output) {
142
- if (!output || !output.trim())
144
+ if (!output?.trim())
143
145
  return false;
144
146
  const text = output.trim();
145
147
  return SEARCHABLE_PATTERNS.some((p) => p.test(text));
146
148
  }
147
149
  /**
148
- * Build the search query for a failing error: up to 3 cleaned error lines
149
- * (paths stripped), prefixed with the runtime and framework detected from the
150
- * step/bash context, suffixed with "how to fix". Falls back to `tool step`
151
- * when the error text is empty. Capped at maxQueryChars.
150
+ * Строит поисковый запрос по падающей ошибке: до 3 чищенных error-строк (пути
151
+ * вырезаны) с префиксом рантайма/фреймворка из step/bash-контекста и суффиксом
152
+ * "how to fix". На пустом тексте — фолбэк `tool step`. Кап maxQueryChars.
152
153
  */
153
154
  export function buildSearchQuery(stepDescription, lastBashCommand, lastErrorOutput, toolName, maxQueryChars) {
154
155
  const errorText = (lastErrorOutput || "")
@@ -180,6 +181,7 @@ export function buildSearchQuery(stepDescription, lastBashCommand, lastErrorOutp
180
181
  }
181
182
  return query.slice(0, maxQueryChars);
182
183
  }
184
+ /** Детектор застревания: счётчики итераций, ошибок, повторов, петель, эскалаций. */
183
185
  export class StuckDetector {
184
186
  threshold;
185
187
  errorThreshold;
@@ -218,14 +220,15 @@ export class StuckDetector {
218
220
  this.resetStepState(stepId);
219
221
  }
220
222
  }
223
+ /** Отмечает вызов тула (для детекции повторов; write/exec сбрасывают read-only стрик). */
221
224
  recordToolCall(name, args) {
222
225
  const argsKey = JSON.stringify(args);
223
226
  this.recentToolCalls.push({ name, argsKey });
224
227
  if (this.recentToolCalls.length > this.maxRecentCalls) {
225
228
  this.recentToolCalls.shift();
226
229
  }
227
- // Track the consecutive read-only streak. A write/exec tool resets it;
228
- // plan/todo bookkeeping counts as read-only (it is not real progress).
230
+ // Трекаем подряд read-only стрик. Пишущий/исполняющий тул сбрасывает;
231
+ // plan/todo-бухгалтерия считается read-only (не реальный прогресс).
229
232
  if (READ_ONLY_TOOLS.has(name)) {
230
233
  this.readOnlyStreak++;
231
234
  }
@@ -234,13 +237,14 @@ export class StuckDetector {
234
237
  }
235
238
  }
236
239
  /**
237
- * True after a long run of read-only tool calls with no write in between.
238
- * The model is exploring/looping without producing anything — inject a
239
- * recovery hint so it stops re-reading and actually edits files.
240
+ * True после длинной серии read-only вызовов без записей между. Модель
241
+ * исследует/петляет, ничего не производя — инжектим recovery-хинт, чтобы
242
+ * перестала перечитывать и реально правила файлы.
240
243
  */
241
244
  hasReadOnlyLoop() {
242
245
  return this.readOnlyStreak >= READ_ONLY_LOOP_THRESHOLD;
243
246
  }
247
+ /** Отмечает ошибку тула (счётчики + сигнатура для автопоиска). */
244
248
  recordToolError(toolName, output) {
245
249
  this.toolErrors.set(toolName, (this.toolErrors.get(toolName) || 0) + 1);
246
250
  this.consecutiveFailures++;
@@ -252,13 +256,14 @@ export class StuckDetector {
252
256
  this.errorSignatureCounts.set(sig, (this.errorSignatureCounts.get(sig) || 0) + 1);
253
257
  }
254
258
  }
259
+ /** Последний текст ошибки. */
255
260
  getLastErrorOutput() {
256
261
  return this.lastErrorOutput;
257
262
  }
258
263
  /**
259
- * Record every bash invocation (successful or not) so stuck detection can
260
- * reason about repeated runs with empty output, entry-point issues, and
261
- * platform mistakes (PowerShell cmdlets in cmd.exe, bash heredocs, ...).
264
+ * Записывает КАЖДЫЙ вызов bash (успех или нет): детекция рассуждает о
265
+ * повторных прогонах с пустым выводом, проблемах entry-point и ошибках
266
+ * платформы (PowerShell-командлеты в cmd.exe, bash-heredoc'и, …).
262
267
  */
263
268
  recordBashOutput(command, output) {
264
269
  const trimmed = output.trim();
@@ -271,13 +276,13 @@ export class StuckDetector {
271
276
  this.lastBashCommand = command;
272
277
  this.lastBashOutput = output;
273
278
  }
274
- /** Outcome of one bash attempt (success or failure), for flailing detection. */
279
+ /** Исход одной bash-попытки (успех/фейл) — для детекции флейлинга. */
275
280
  recordBashAttempt(success) {
276
281
  this.bashRecent.push(success);
277
282
  if (this.bashRecent.length > BASH_FLAIL_WINDOW)
278
283
  this.bashRecent.shift();
279
284
  }
280
- /** True when most recent bash attempts failed — the model is flailing. */
285
+ /** True, если свежие bash-попытки в массе фейлятся — модель флейлит. */
281
286
  hasBashFlailing() {
282
287
  if (this.bashRecent.length < BASH_FLAIL_THRESHOLD)
283
288
  return false;
@@ -287,20 +292,22 @@ export class StuckDetector {
287
292
  failures++;
288
293
  return failures >= BASH_FLAIL_THRESHOLD;
289
294
  }
295
+ /** Последняя bash-команда. */
290
296
  getLastBashCommand() {
291
297
  return this.lastBashCommand;
292
298
  }
299
+ /** Вывод последней bash-команды. */
293
300
  getLastBashOutput() {
294
301
  return this.lastBashOutput;
295
302
  }
303
+ /** Последний упавший тул. */
296
304
  getLastFailedTool() {
297
305
  return this.lastFailedTool;
298
306
  }
299
307
  /**
300
- * First error signature whose repetition count reached the web-search
301
- * threshold and that has NOT been searched yet (or was unmarked for a
302
- * retry). This is what triggers the automatic web search — not iterations
303
- * and not the per-tool error count.
308
+ * Первая сигнатура ошибки, чей счётчик повторов достиг порога веб-поиска и
309
+ * которая ЕЩЁ не искалась (или размаркирована для ретрая). Именно она
310
+ * триггерит автопоиск — не итерации и не per-tool счётчик ошибок.
304
311
  */
305
312
  getRepeatedErrorSignature() {
306
313
  for (const [sig, count] of this.errorSignatureCounts) {
@@ -310,63 +317,75 @@ export class StuckDetector {
310
317
  }
311
318
  return null;
312
319
  }
320
+ /** Счётчик повторов сигнатуры ошибки. */
313
321
  getErrorSignatureCount(sig) {
314
322
  return this.errorSignatureCounts.get(sig) || 0;
315
323
  }
324
+ /** Помечает сигнатуру как уже искавшуюся. */
316
325
  markErrorSearched(sig) {
317
326
  this.searchedErrorSignatures.add(sig);
318
327
  }
328
+ /** Снимает метку поиска (разрешает один ретрай). */
319
329
  unmarkErrorSearched(sig) {
320
330
  this.searchedErrorSignatures.delete(sig);
321
331
  }
332
+ /** True, если сигнатура уже искалась. */
322
333
  hasErrorSearched(sig) {
323
334
  return this.searchedErrorSignatures.has(sig);
324
335
  }
336
+ /** Итераций на текущем шаге. */
325
337
  getIterationsOnCurrentStep() {
326
338
  return this.iterationsOnCurrentStep;
327
339
  }
340
+ /** Отмечает успешный тул: сброс фейлов, затухание per-tool счётчиков и итераций. */
328
341
  recordToolSuccess() {
329
342
  this.consecutiveFailures = 0;
330
343
  this.lastFailedTool = "";
331
- this.escalationCount = 0;
332
- // Decay per-tool error counters on productive calls: a tool that failed
333
- // early must not keep the "Tool X failed N times" warning alive forever
334
- // (ses_mt4fn58c — bash failed 3x, warning re-logged after 30 successful
335
- // writes). Fresh failures rebuild the count.
344
+ // NOTE: escalationCount осознанно НЕ сбрасываем. Один тривиальный успех
345
+ // (напр. один read_file) раньше его чистил, и shouldEscalate() на флейлящей
346
+ // сессии почти не достигался — в ущерб анти-гейминг свойству, которое
347
+ // resetStepState() явно хранит.
348
+ // Затухание per-tool счётчиков на продуктивных вызовах: рано упавший тул не
349
+ // держит варнинг "Tool X failed N times" вечно (ses_mt4fn58c — bash упал 3x,
350
+ // варнинг перелоггировался после 30 успешных записей). Свежие фейлы счёт
351
+ // набирают заново.
336
352
  for (const [tool, count] of this.toolErrors) {
337
353
  if (count <= 1)
338
354
  this.toolErrors.delete(tool);
339
355
  else
340
356
  this.toolErrors.set(tool, count - 1);
341
357
  }
342
- // Decay iteration counter on productive tool calls — reading new files,
343
- // writing, editing all count as progress. Prevents false-positive stuck
344
- // warnings during legitimate file exploration (e.g. reading 12 files in
345
- // an analysis step).
358
+ // Затухание счётчика итераций на продуктивных вызовах: чтение новых файлов,
359
+ // записи, правки — всё прогресс. Защита от ложных stuck-варнингов при
360
+ // легитимной разведке файлов (напр. чтение 12 файлов в analytic-шаге).
346
361
  if (this.iterationsOnCurrentStep > 0) {
347
362
  this.iterationsOnCurrentStep = Math.max(0, this.iterationsOnCurrentStep - 1);
348
363
  }
349
364
  }
365
+ /** Ставит текущий шаг (сброс per-step состояния при смене). */
350
366
  setCurrentStep(stepId, description) {
351
367
  if (stepId !== this.currentStepId) {
352
368
  this.resetStepState(stepId);
353
369
  }
354
370
  this.currentStepDescription = description;
355
371
  }
372
+ /** True, если итераций на шаге больше порога. */
356
373
  isStuck() {
357
374
  return this.iterationsOnCurrentStep > this.threshold;
358
375
  }
376
+ /** True, если тул (или любой) ошибался ≥ порога. */
359
377
  hasRepetitiveErrors(toolName) {
360
378
  if (toolName) {
361
379
  return (this.toolErrors.get(toolName) || 0) >= this.errorThreshold;
362
380
  }
363
381
  return Array.from(this.toolErrors.values()).some((c) => c >= this.errorThreshold);
364
382
  }
383
+ /** True при повторяющихся вызовах: подряд идентичных или перемежающихся (A,B,A,B,A). */
365
384
  hasRepetitiveToolCalls() {
366
385
  if (this.recentToolCalls.length < this.repetitionThreshold)
367
386
  return false;
368
387
  const last = this.recentToolCalls[this.recentToolCalls.length - 1];
369
- // Check consecutive identical calls (original logic)
388
+ // Проверка подряд идентичных (исходная логика)
370
389
  let consecutiveCount = 1;
371
390
  for (let i = this.recentToolCalls.length - 2; i >= 0; i--) {
372
391
  if (this.recentToolCalls[i].name === last.name &&
@@ -379,8 +398,8 @@ export class StuckDetector {
379
398
  }
380
399
  if (consecutiveCount >= this.repetitionThreshold)
381
400
  return true;
382
- // Also detect interleaved patterns (A, B, A, B, A) — count any single
383
- // tool+args combo in the window and flag if it appears >= threshold times
401
+ // Плюс перемежающиеся паттерны (A, B, A, B, A): считаем каждую связку
402
+ // тул+args в окне и флагуем при ≥ порога
384
403
  const counts = new Map();
385
404
  for (const call of this.recentToolCalls) {
386
405
  const key = `${call.name}::${call.argsKey}`;
@@ -392,24 +411,31 @@ export class StuckDetector {
392
411
  }
393
412
  return false;
394
413
  }
414
+ /** True при ≥ порога подряд фейлов. */
395
415
  hasConsecutiveFailures() {
396
416
  return this.consecutiveFailures >= this.errorThreshold;
397
417
  }
418
+ /** Число подряд фейлов. */
398
419
  getConsecutiveFailuresCount() {
399
420
  return this.consecutiveFailures;
400
421
  }
422
+ /** Отмечает эскалацию (счётчик для shouldEscalate). */
401
423
  recordEscalation() {
402
424
  this.escalationCount++;
403
425
  }
426
+ /** True, если эскалаций набралось на порог. */
404
427
  shouldEscalate() {
405
428
  return this.escalationCount >= this.escalationThreshold;
406
429
  }
430
+ /** Сбрасывает счётчик эскалаций. */
407
431
  resetEscalation() {
408
432
  this.escalationCount = 0;
409
433
  }
434
+ /** Число эскалаций. */
410
435
  getEscalationCount() {
411
436
  return this.escalationCount;
412
437
  }
438
+ /** Хинты по описанию шага + состояниям, ещё не покрытым recovery-сообщением. */
413
439
  getHints() {
414
440
  const hints = [];
415
441
  const desc = this.currentStepDescription.toLowerCase();
@@ -425,11 +451,11 @@ export class StuckDetector {
425
451
  if (desc.includes("deploy") || desc.includes("publish")) {
426
452
  hints.push(t("exec.hint_deploy"));
427
453
  }
428
- // State-based bullets. The recovery message (getRecoveryMessage) already
429
- // reports the PRIMARY reason (priority: stuck > tool-errors > consecutive
430
- // > repetitive > read-only). Repeating that same state here as a hint
431
- // would duplicate the guidance in the same cooldown block. Only add the
432
- // bullet when the recovery message is NOT already covering that state.
454
+ // State-based буллеты. Recovery-сообщение (getRecoveryMessage) уже репортит
455
+ // ПЕРВИЧНУЮ причину (приоритет: stuck > tool-errors > consecutive >
456
+ // repetitive > read-only). Дублировать то же состояние хинтом — задвоить
457
+ // гайданс в одном cooldown-блоке. Буллет добавляем, только если recovery
458
+ // это состояние НЕ покрывает.
433
459
  const primary = this.getPrimaryReason();
434
460
  if (this.hasRepetitiveToolCalls() && primary !== "repetitive") {
435
461
  hints.push(t("exec.hint_repetitive"));
@@ -442,81 +468,83 @@ export class StuckDetector {
442
468
  }
443
469
  return hints;
444
470
  }
471
+ /** Actionable-хинты по последней ошибке: рантаймы, синтаксис, сеть, шелл-несоответствия. */
445
472
  getActionableHints() {
446
473
  const hints = [];
447
474
  const error = this.lastErrorOutput;
448
- // Bash-specific hints do not depend on a tool failure — they fire even
449
- // when the command "succeeded" with empty output (missing entry point).
475
+ // Bash-хинты от фейла тула не зависят — срабатывают, даже когда команда
476
+ // "успешна" с пустым выводом (потерянный entry point).
450
477
  const isScriptRun = /(^|[\s&])(bun|node|tsx|ts-node|deno|python|python3)\S*\s+[^|&]+\s+[^\s]+$/.test(this.lastBashCommand);
451
478
  if (isScriptRun && !this.lastBashOutput.trim() && this.emptyBashRunCount >= 2) {
452
479
  hints.push(`The command "${this.lastBashCommand}" ran ${this.emptyBashRunCount} times with EMPTY output. The program likely has no entry point — read the file with read_file and check that it actually calls its main function with process.argv / CLI arguments and prints results (console.log). Then run it again.`);
453
480
  }
454
481
  if (!error)
455
482
  return hints;
456
- // Runtime incompatibility — tool/module crashes on this Node version
483
+ // Несовместимость рантайма — тул/модуль крашится на этой версии Node
457
484
  if (/Cannot read properties of undefined|is not a function|is not a constructor/i.test(error)) {
458
485
  if (/node_modules/.test(error)) {
459
486
  hints.push("A dependency is incompatible with your Node.js version. Check if there is an alternative package or use a different runtime.");
460
487
  }
461
488
  }
462
- // TypeScript syntax errors — model generated invalid code
489
+ // Синтаксические ошибки TypeScript — модель сгенерила невалидный код
463
490
  if (/TS1005|TS1003|TS1109|TS1128|TS1434/i.test(error)) {
464
491
  hints.push("TypeScript syntax error. Read the error message carefully — it tells you the exact line and column. Fix the syntax before retrying.");
465
492
  }
466
- // TypeScript type errors — model doesn't understand the types
493
+ // Ошибки типов TypeScript — модель не понимает типы
467
494
  if (/TS2322|TS2345|TS2769|TS7006|TS7016/i.test(error)) {
468
495
  hints.push("TypeScript type mismatch. Check the type signature of the function/API you are using. If a package lacks type declarations, install @types/<package> or use skipLibCheck.");
469
496
  }
470
- // Module not found — missing dependency or wrong import
497
+ // Модуль не найден — нет зависимости или неверный импорт
471
498
  if (/Cannot find module|Module not found|ERR_MODULE_NOT_FOUND/i.test(error)) {
472
499
  hints.push("Module not found. Run npm install or check if the import path is correct.");
473
500
  }
474
- // Permission / access errors
501
+ // Ошибки прав доступа
475
502
  if (/EACCES|EPERM|permission denied/i.test(error)) {
476
503
  hints.push("Permission denied. Check file permissions or run with appropriate privileges.");
477
504
  }
478
- // Network errors
505
+ // Сетевые ошибки
479
506
  if (/ECONNREFUSED|ETIMEDOUT|ENOTFOUND|fetch failed/i.test(error)) {
480
507
  hints.push("Network error. Check if the server is running and accessible.");
481
508
  }
482
- // Command not found
509
+ // Команда не найдена
483
510
  if (/is not recognized|command not found|not found in path/i.test(error)) {
484
511
  hints.push("Command not found. Check if the tool is installed and in PATH.");
485
512
  }
486
- // File rewrite loop — same file keeps failing
513
+ // Петля перезаписи: один файл безуспешно переписывается
487
514
  if (this.hasExcessiveRewrites()) {
488
515
  const file = this.getExcessiveRewriteFile();
489
516
  hints.push(`File ${file} has been rewritten ${this.getFileRewriteCount(file)} times without success. Stop rewriting and try a fundamentally different approach.`);
490
517
  }
491
- // Repeated identical bash runs with empty output — likely a missing
492
- // entry point (the program never calls its main function).
493
- // PowerShell cmdlets used inside cmd.exe (Windows shell mismatch).
518
+ // Повторные идентичные bash-прогоны с пустым выводом — вероятно, потерянный
519
+ // entry point (программа не вызывает main).
520
+ // PowerShell-командлеты внутри cmd.exe (шелл-несоответствие Windows).
494
521
  if (/Write-Host|Get-Content|Select-String|Out-File|Set-Content/i.test(error)) {
495
522
  hints.push("That looks like a PowerShell cmdlet — the shell here is cmd.exe. Use echo/type for output or the read_file/write_file tools instead.");
496
523
  }
497
- // Model invented a Bun API that does not exist.
524
+ // Модель выдумала несуществующий Bun API.
498
525
  if (/Bun\.\w+ is not a function|Bun\.\w+ is not defined|Bun\.\w+ is not a constructor/i.test(error)) {
499
526
  hints.push("That Bun API does not exist. Verify the API name in the Bun docs — common ones are Bun.file, Bun.write, Bun.spawn, Bun.serve. For file checks use fs.existsSync from node:fs.");
500
527
  }
501
- // The model keeps writing a file whose output shows a type/syntax
502
- // error — read the actual error and fix it, don't rewrite blindly.
528
+ // Модель продолжает писать файл, в выводе которого type/syntax ошибка —
529
+ // читай реальную ошибку и чини её, не переписывай вслепую.
503
530
  if (/error TS\d+|typecheck failed|syntax check failed/i.test(error)) {
504
531
  hints.push("The file still has a type/syntax error (see the error line in the output). Read the file with read_file around the reported line, fix the actual error, then re-run — rewriting the whole file blindly usually makes it worse.");
505
532
  }
506
- // Bash heredoc in cmd.exe.
533
+ // Bash-heredoc в cmd.exe.
507
534
  if (/unexpected.*<<|Непредвиденное появление/i.test(error)) {
508
535
  hints.push("Heredoc (<< EOF) is a bash feature — this shell is cmd.exe and does not support it. Write the file with write_file instead.");
509
536
  }
510
537
  return hints;
511
538
  }
539
+ /** Альтернативный тул при рантайм-краше (ts-node→tsx, jest→vitest, …). Null, если нечего предложить. */
512
540
  getToolAlternative() {
513
541
  const tool = this.lastFailedTool;
514
542
  const error = this.lastErrorOutput;
515
543
  if (!tool)
516
544
  return null;
517
- // If a tool fails with a runtime crash, suggest alternatives
545
+ // Упавший тул с рантайм-крашем — предлагаем альтернативы
518
546
  if (/Cannot read properties of undefined|is not a function|TypeError|ReferenceError/i.test(error)) {
519
- // Map of tools to their common alternatives
547
+ // Карта упавших тулов на частые альтернативы
520
548
  const alternatives = {
521
549
  "ts-node": "tsx",
522
550
  jest: "vitest",
@@ -536,16 +564,20 @@ export class StuckDetector {
536
564
  }
537
565
  return null;
538
566
  }
567
+ /** Отмечает перезапись файла (счётчик для детекции петли). */
539
568
  recordFileRewrite(filePath) {
540
569
  const count = (this.fileRewriteCount.get(filePath) || 0) + 1;
541
570
  this.fileRewriteCount.set(filePath, count);
542
571
  }
572
+ /** Число перезаписей файла. */
543
573
  getFileRewriteCount(filePath) {
544
574
  return this.fileRewriteCount.get(filePath) || 0;
545
575
  }
576
+ /** True, если какой-то файл переписывается ≥ порога. */
546
577
  hasExcessiveRewrites() {
547
578
  return Array.from(this.fileRewriteCount.values()).some((c) => c >= this.fileRewriteThreshold);
548
579
  }
580
+ /** Файл с избыточными перезаписями, или null. */
549
581
  getExcessiveRewriteFile() {
550
582
  for (const [file, count] of this.fileRewriteCount) {
551
583
  if (count >= this.fileRewriteThreshold)
@@ -553,18 +585,25 @@ export class StuckDetector {
553
585
  }
554
586
  return null;
555
587
  }
588
+ /** Сообщение о повторяющемся туле (+хинт про process_wait для process_log). */
556
589
  getRepetitiveToolMessage() {
557
590
  if (!this.hasRepetitiveToolCalls())
558
591
  return "";
559
592
  const last = this.recentToolCalls[this.recentToolCalls.length - 1];
560
- return t("exec.repetitive_tool", {
593
+ const base = t("exec.repetitive_tool", {
561
594
  tool: last.name,
562
595
  count: this.repetitionThreshold,
563
596
  });
597
+ // Опрос фонового процесса по одному process_log за раз жжёт LLM-вызов на
598
+ // опрос — указываем на одиночное блокирующее ожидание.
599
+ if (last.name === "process_log") {
600
+ return `${base} ${t("exec.repetitive_process_log_hint")}`;
601
+ }
602
+ return base;
564
603
  }
565
604
  /**
566
- * Stable warning identity for anti-spam dedup: the same key re-logged every
567
- * iteration is spam, a NEW key (or a long gap) deserves a re-warn.
605
+ * Стабильная идентичность варнинга для анти-спам дедупа: тот же ключ каждую
606
+ * итерацию — спам, НОВЫЙ ключ (или длинная пауза) заслуживает re-warn.
568
607
  */
569
608
  getWarningKey() {
570
609
  if (this.isStuck())
@@ -582,6 +621,7 @@ export class StuckDetector {
582
621
  return "read-only";
583
622
  return "";
584
623
  }
624
+ /** Причина застревания для лога (пусто если здоров). */
585
625
  getStuckReason() {
586
626
  if (this.isStuck()) {
587
627
  return t("exec.stuck", {
@@ -594,6 +634,22 @@ export class StuckDetector {
594
634
  if (errorTool) {
595
635
  return t("exec.tool_errors", { tool: errorTool[0], count: errorTool[1] });
596
636
  }
637
+ if (this.hasBashFlailing()) {
638
+ const fails = this.bashRecent.filter((ok) => !ok).length;
639
+ return t("exec.bash_flailing", {
640
+ fails: String(fails),
641
+ window: String(this.bashRecent.length),
642
+ });
643
+ }
644
+ if (this.hasConsecutiveFailures()) {
645
+ return t("exec.consecutive_failures", { count: String(this.consecutiveFailures) });
646
+ }
647
+ // Повторные идентичные вызовы — петля, даже если ничего не фейлится (напр.
648
+ // цикл опроса process_log): без этой ветки оставались невидимы —
649
+ // getWarningKey репортил "repetitive", но причина не логгировалась.
650
+ if (this.hasRepetitiveToolCalls()) {
651
+ return this.getRepetitiveToolMessage();
652
+ }
597
653
  if (this.hasReadOnlyLoop()) {
598
654
  return t("exec.read_only_loop", {
599
655
  count: String(this.readOnlyStreak),
@@ -601,6 +657,7 @@ export class StuckDetector {
601
657
  }
602
658
  return "";
603
659
  }
660
+ /** Recovery-сообщение по первичной причине (пусто если здоров). */
604
661
  getRecoveryMessage() {
605
662
  const reason = this.getPrimaryReason();
606
663
  if (reason === "stuck") {
@@ -610,7 +667,7 @@ export class StuckDetector {
610
667
  description: this.currentStepDescription,
611
668
  });
612
669
  }
613
- // Per-tool error recovery (more specific — e.g., "bash failed 3 times")
670
+ // Per-tool recovery (конкретнее — напр. "bash упал 3 раза")
614
671
  if (reason === "tool-errors") {
615
672
  const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
616
673
  return t("exec.tool_errors_recovery", {
@@ -618,7 +675,7 @@ export class StuckDetector {
618
675
  count: errorTool[1],
619
676
  });
620
677
  }
621
- // Consecutive failures from different tools (generic — e.g., "5 consecutive failures")
678
+ // Подряд фейлы разных тулов (общее — напр. "5 consecutive failures")
622
679
  if (reason === "consecutive") {
623
680
  return t("exec.consecutive_failures_recovery", {
624
681
  count: this.consecutiveFailures,
@@ -642,11 +699,56 @@ export class StuckDetector {
642
699
  return "";
643
700
  }
644
701
  /**
645
- * Single source of truth for the recovery reason priority. Used by BOTH
646
- * getRecoveryMessage() and getHints() so the two cannot drift apart: the
647
- * state bullet that getRecoveryMessage already reports is NOT repeated as a
648
- * hint (priority: stuck > tool-errors > consecutive > repetitive >
649
- * read-only).
702
+ * Классифицирует текущее состояние фейла в стабильный {@link FailureType}
703
+ * для диагностики/логов. Null если здоров. `iteration` даёт вызывающий
704
+ * (детектор глобальный счётчик цикла не ведёт).
705
+ */
706
+ classifyFailure(iteration = 0) {
707
+ const primary = this.getPrimaryReason();
708
+ if (!primary)
709
+ return null;
710
+ switch (primary) {
711
+ case "stuck":
712
+ return { type: "timeout", iteration };
713
+ case "tool-errors": {
714
+ const entry = Array.from(this.toolErrors.entries()).find(([, c]) => c >= this.errorThreshold);
715
+ return { type: "error_loop", tool: entry?.[0], count: entry?.[1], iteration };
716
+ }
717
+ case "consecutive":
718
+ return { type: "error_loop", count: this.consecutiveFailures, iteration };
719
+ case "repetitive": {
720
+ const last = this.recentToolCalls[this.recentToolCalls.length - 1];
721
+ return { type: "repeat", tool: last?.name, count: this.repetitionThreshold, iteration };
722
+ }
723
+ case "bash-flailing": {
724
+ const fails = this.bashRecent.filter((ok) => !ok).length;
725
+ return { type: "bash_flailing", count: fails, iteration };
726
+ }
727
+ case "read-only":
728
+ return { type: "read_only", count: this.readOnlyStreak, iteration };
729
+ }
730
+ }
731
+ /** Компактный маркер `[STUCK: type=..., tool=..., count=..., iteration=N]`. */
732
+ formatFailureDiagnostic(c) {
733
+ const parts = [`type=${c.type}`];
734
+ if (c.tool)
735
+ parts.push(`tool=${c.tool}`);
736
+ if (typeof c.count === "number")
737
+ parts.push(`count=${c.count}`);
738
+ if (c.iteration > 0)
739
+ parts.push(`iteration=${c.iteration}`);
740
+ return `[STUCK: ${parts.join(", ")}]`;
741
+ }
742
+ /** Диагностический маркер текущего состояния, или "" если здоров. */
743
+ getStuckDiagnostic(iteration = 0) {
744
+ const c = this.classifyFailure(iteration);
745
+ return c ? this.formatFailureDiagnostic(c) : "";
746
+ }
747
+ /**
748
+ * Единый источник правды приоритета recovery-причины. Общий для
749
+ * getRecoveryMessage() и getHints(), чтобы не расходились: bullet состояния,
750
+ * которое getRecoveryMessage уже репортит, хинтом НЕ дублируется
751
+ * (приоритет: stuck > tool-errors > consecutive > repetitive > read-only).
650
752
  */
651
753
  getPrimaryReason() {
652
754
  if (this.isStuck())
@@ -664,6 +766,7 @@ export class StuckDetector {
664
766
  return "read-only";
665
767
  return null;
666
768
  }
769
+ /** Полный сброс всего состояния детектора. */
667
770
  reset() {
668
771
  this.currentStepId = null;
669
772
  this.iterationsOnCurrentStep = 0;
@@ -683,10 +786,10 @@ export class StuckDetector {
683
786
  this.searchedErrorSignatures.clear();
684
787
  }
685
788
  /**
686
- * Reset only step-progress state, keeping the tool-call history and the
687
- * read-only streak. Used when the plan is complete but the loop continues
688
- * (e.g. the audit gate rejected a finished task): step-stuck warnings are
689
- * silenced, but a read-without-write loop is still detected.
789
+ * Сбрасывает только step-прогресс, храня историю вызовов и read-only стрик.
790
+ * Нужно, когда план готов, но цикл продолжается (напр. audit-gate реджектнул
791
+ * готовую задачу): step-stuck варнинги глушатся, а петля чтения-без-записи
792
+ * всё равно детектится.
690
793
  */
691
794
  resetStepProgress() {
692
795
  this.currentStepId = null;
@@ -705,9 +808,9 @@ export class StuckDetector {
705
808
  this.searchedErrorSignatures.clear();
706
809
  }
707
810
  /**
708
- * Reset all per-step state when moving to a new step. Prevents errors and
709
- * tool repetitions from one step leaking into the next (false-positive
710
- * stuck/recovery on the first iteration of the new step).
811
+ * Сбрасывает всё per-step состояние при переходе на новый шаг. Не даёт
812
+ * ошибкам и повторам одного шага протекать в следующий (ложные
813
+ * stuck/recovery на первой итерации нового шага).
711
814
  */
712
815
  resetStepState(stepId) {
713
816
  this.currentStepId = stepId;
@@ -725,7 +828,7 @@ export class StuckDetector {
725
828
  this.readOnlyStreak = 0;
726
829
  this.errorSignatureCounts.clear();
727
830
  this.searchedErrorSignatures.clear();
728
- // Preserve escalationCount — prevents agent from "gaming" stuck detection
729
- // by marking a step done and moving to the next one
831
+ // escalationCount храним — иначе агент "геймит" детекцию, маркируя шаг
832
+ // done и переходя к следующему
730
833
  }
731
834
  }