micro-models-agent 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/assets/app.js +22 -4
  3. package/dist/assets/index.html +29 -0
  4. package/dist/assets/js/dom.js +2 -1
  5. package/dist/assets/js/feed.js +38 -26
  6. package/dist/assets/js/format.js +1 -1
  7. package/dist/assets/js/rail.js +114 -0
  8. package/dist/assets/js/settings-controls.js +2 -2
  9. package/dist/assets/js/settings.js +246 -51
  10. package/dist/assets/js/store.js +3 -0
  11. package/dist/assets/js/tabs.js +104 -2
  12. package/dist/assets/styles.css +215 -66
  13. package/dist/cli/cache-line.js +1 -0
  14. package/dist/cli/command-suggest.js +5 -4
  15. package/dist/cli/commands.js +231 -122
  16. package/dist/cli/completer.js +132 -31
  17. package/dist/cli/index.js +1 -0
  18. package/dist/cli/json-payload.js +4 -4
  19. package/dist/cli/main.js +166 -61
  20. package/dist/cli/onboarding-commands.js +126 -0
  21. package/dist/cli/plugin-commands.js +2 -1
  22. package/dist/cli/relaunch.js +16 -13
  23. package/dist/cli/repl-commands-lsp.js +75 -0
  24. package/dist/cli/repl-commands-skills.js +105 -0
  25. package/dist/cli/repl-commands.js +589 -266
  26. package/dist/cli/repl.js +436 -177
  27. package/dist/cli/run-result.js +5 -6
  28. package/dist/cli/security-commands.js +37 -28
  29. package/dist/cli/setup-order.js +4 -55
  30. package/dist/cli/setup-prompt.js +3 -2
  31. package/dist/cli/setup.js +27 -107
  32. package/dist/cli/web-command.js +98 -0
  33. package/dist/config/budget.js +5 -5
  34. package/dist/config/config.js +103 -96
  35. package/dist/config/defaults.js +38 -8
  36. package/dist/config/domains.js +24 -27
  37. package/dist/config/env.js +46 -0
  38. package/dist/config/experts.js +2 -0
  39. package/dist/config/index.js +1 -0
  40. package/dist/config/paths.js +56 -0
  41. package/dist/config/redact.js +44 -0
  42. package/dist/config/registry-extra.js +334 -0
  43. package/dist/config/registry.js +584 -0
  44. package/dist/config/security.js +35 -36
  45. package/dist/config/timeouts.js +74 -0
  46. package/dist/config/utils.js +16 -15
  47. package/dist/core/agent/artifact-progress.js +59 -0
  48. package/dist/core/agent/artifact-resolve.js +79 -0
  49. package/dist/core/agent/audit-gate.js +7 -3
  50. package/dist/core/agent/compaction.js +2 -2
  51. package/dist/core/agent/constants.js +33 -4
  52. package/dist/core/agent/context-log.js +454 -0
  53. package/dist/core/agent/context-renderer.js +5 -0
  54. package/dist/core/agent/critic-gate.js +132 -0
  55. package/dist/core/agent/hallucination-gate.js +148 -38
  56. package/dist/core/agent/loop-state.js +15 -0
  57. package/dist/core/agent/prefix-monitor.js +1 -1
  58. package/dist/core/agent/reasoning-resolver.js +10 -1
  59. package/dist/core/agent/token-tracker.js +7 -4
  60. package/dist/core/agent/tool-batch.js +93 -18
  61. package/dist/core/agent/tool-images.js +18 -0
  62. package/dist/core/agent/tool-output.js +65 -11
  63. package/dist/core/agent/tool-pairing.js +60 -0
  64. package/dist/core/agent-moe.js +25 -24
  65. package/dist/core/agent.js +429 -108
  66. package/dist/core/bootstrap/agent-phase.js +66 -0
  67. package/dist/core/bootstrap/config-phase.js +86 -0
  68. package/dist/core/bootstrap/modules-phase.js +269 -0
  69. package/dist/core/bootstrap/provider-phase.js +183 -0
  70. package/dist/core/bootstrap/session-phase.js +204 -0
  71. package/dist/core/bootstrap/state.js +94 -0
  72. package/dist/core/bootstrap/workspace-phase.js +115 -0
  73. package/dist/core/bootstrap.js +92 -660
  74. package/dist/core/crash-handler.js +17 -7
  75. package/dist/core/environment.js +37 -12
  76. package/dist/core/host-plan.js +43 -0
  77. package/dist/core/host.js +23 -0
  78. package/dist/core/index.js +1 -0
  79. package/dist/core/prompt-builder.js +17 -1
  80. package/dist/core/prompt-overflow.js +42 -43
  81. package/dist/core/session-logger.js +122 -30
  82. package/dist/core/system-prompt.js +117 -0
  83. package/dist/core/version.js +6 -4
  84. package/dist/core/web-host.js +34 -0
  85. package/dist/core/workspace.js +19 -15
  86. package/dist/i18n/en.json +106 -21
  87. package/dist/i18n/index.js +37 -3
  88. package/dist/i18n/ru.json +109 -21
  89. package/dist/index.js +7 -3
  90. package/dist/llm/image-utils.js +55 -64
  91. package/dist/llm/llm-errors.js +9 -0
  92. package/dist/llm/model-loader.js +10 -32
  93. package/dist/llm/models-dev.js +121 -0
  94. package/dist/llm/openai-compat.js +146 -73
  95. package/dist/llm/orchestrator.js +33 -11
  96. package/dist/llm/provider-budget.js +2 -2
  97. package/dist/llm/provider-context.js +233 -0
  98. package/dist/llm/provider.js +1 -1
  99. package/dist/llm/response.js +5 -0
  100. package/dist/llm/token-counter.js +27 -11
  101. package/dist/logger/app-logger.js +48 -22
  102. package/dist/logger/file-log.js +28 -19
  103. package/dist/logger/index.js +1 -0
  104. package/dist/main.js +24285 -22447
  105. package/dist/migration/backup.js +14 -3
  106. package/dist/migration/detect.js +12 -3
  107. package/dist/migration/index.js +1 -0
  108. package/dist/modules/artifacts/store.js +9 -8
  109. package/dist/modules/browser/actions.js +6 -0
  110. package/dist/modules/browser/bridge-client.js +14 -12
  111. package/dist/modules/browser/bridge-path.js +4 -4
  112. package/dist/modules/browser/console-buffer.js +37 -0
  113. package/dist/modules/browser/cookie-store.js +5 -0
  114. package/dist/modules/browser/driver-types.js +1 -0
  115. package/dist/modules/browser/driver.js +19 -11
  116. package/dist/modules/browser/index.js +3 -1
  117. package/dist/modules/browser/module.js +2 -1
  118. package/dist/modules/browser/session.js +10 -30
  119. package/dist/modules/browser/snapshot.js +19 -13
  120. package/dist/modules/browser/types.js +1 -0
  121. package/dist/modules/certification/cli.js +28 -20
  122. package/dist/modules/certification/fact-checker.js +9 -1
  123. package/dist/modules/certification/loader.js +9 -2
  124. package/dist/modules/certification/manifest.js +83 -21
  125. package/dist/modules/certification/runner.js +23 -13
  126. package/dist/modules/certification/scenarios.js +20 -20
  127. package/dist/modules/certification/syntax-scenarios.js +1 -2
  128. package/dist/modules/context/chunk-query.js +11 -9
  129. package/dist/modules/context/fact-extractor.js +37 -39
  130. package/dist/modules/context/index.js +1 -0
  131. package/dist/modules/context/manager.js +106 -80
  132. package/dist/modules/critic/client.js +176 -0
  133. package/dist/modules/critic/evidence.js +81 -0
  134. package/dist/modules/critic/provider.js +45 -0
  135. package/dist/modules/critic/tools.js +78 -0
  136. package/dist/modules/critic/types.js +1 -0
  137. package/dist/modules/execution/audit-runners.js +28 -34
  138. package/dist/modules/execution/auditor.js +53 -49
  139. package/dist/modules/execution/execution-plugin.js +142 -102
  140. package/dist/modules/execution/index.js +1 -1
  141. package/dist/modules/execution/input-from.js +37 -13
  142. package/dist/modules/execution/module.js +368 -191
  143. package/dist/modules/execution/moe-executor.js +53 -26
  144. package/dist/modules/execution/plan-actions.js +320 -202
  145. package/dist/modules/execution/plan-checks.js +35 -0
  146. package/dist/modules/execution/plan-coverage.js +36 -33
  147. package/dist/modules/execution/plan-steps.js +11 -3
  148. package/dist/modules/execution/plan-store.js +83 -17
  149. package/dist/modules/execution/plan-tool.js +89 -35
  150. package/dist/modules/execution/plan-validator.js +30 -10
  151. package/dist/modules/execution/planner.js +22 -13
  152. package/dist/modules/execution/stuck-detector.js +196 -93
  153. package/dist/modules/execution/tracker.js +22 -8
  154. package/dist/modules/execution/verifier.js +69 -48
  155. package/dist/modules/execution/windows-commands.js +12 -12
  156. package/dist/modules/hallucination/artifact-claim.js +93 -0
  157. package/dist/modules/hallucination/confidence.js +13 -11
  158. package/dist/modules/hallucination/consistency.js +10 -2
  159. package/dist/modules/hallucination/detector.js +10 -5
  160. package/dist/modules/hallucination/factual.js +43 -26
  161. package/dist/modules/hallucination/index.js +1 -0
  162. package/dist/modules/hallucination/js-identifiers.js +44 -45
  163. package/dist/modules/hallucination/llm-judge.js +17 -12
  164. package/dist/modules/hallucination/success-claim.js +59 -0
  165. package/dist/modules/indexer/cache.js +52 -4
  166. package/dist/modules/indexer/index.js +1 -0
  167. package/dist/modules/indexer/map-command.js +2 -4
  168. package/dist/modules/indexer/map-select.js +16 -16
  169. package/dist/modules/indexer/module.js +47 -12
  170. package/dist/modules/indexer/project-profile.js +8 -7
  171. package/dist/modules/indexer/symbols.js +15 -15
  172. package/dist/modules/indexer/walker.js +159 -29
  173. package/dist/modules/lsp/check-tool.js +7 -5
  174. package/dist/modules/lsp/client.js +39 -39
  175. package/dist/modules/lsp/command.js +17 -24
  176. package/dist/modules/lsp/config.js +12 -11
  177. package/dist/modules/lsp/index.js +1 -0
  178. package/dist/modules/lsp/module.js +48 -46
  179. package/dist/modules/lsp/probe.js +13 -14
  180. package/dist/modules/lsp/project-root.js +14 -17
  181. package/dist/modules/lsp/startup-check.js +18 -16
  182. package/dist/modules/mcp/client.js +59 -62
  183. package/dist/modules/mcp/index.js +1 -0
  184. package/dist/modules/mcp/module.js +66 -37
  185. package/dist/modules/mcp/registry.js +5 -0
  186. package/dist/modules/memory/index.js +1 -0
  187. package/dist/modules/memory/module.js +12 -8
  188. package/dist/modules/memory/search.js +4 -1
  189. package/dist/modules/memory/store.js +40 -2
  190. package/dist/modules/onboarding/init.js +47 -0
  191. package/dist/modules/onboarding/meet.js +30 -0
  192. package/dist/modules/onboarding/mode.js +30 -0
  193. package/dist/modules/onboarding/prompts.js +43 -0
  194. package/dist/modules/pipelines/engine.js +10 -0
  195. package/dist/modules/pipelines/index.js +1 -0
  196. package/dist/modules/pipelines/parser.js +4 -1
  197. package/dist/modules/pipelines/template.js +4 -0
  198. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  199. package/dist/modules/plugins/builtin/notify.js +2 -0
  200. package/dist/modules/plugins/index.js +1 -0
  201. package/dist/modules/plugins/loader.js +18 -3
  202. package/dist/modules/plugins/manager.js +37 -17
  203. package/dist/modules/pricing/index.js +10 -11
  204. package/dist/modules/pricing/prices.js +12 -12
  205. package/dist/modules/processes/index.js +1 -0
  206. package/dist/modules/processes/kill-tree.js +27 -31
  207. package/dist/modules/processes/registry.js +44 -25
  208. package/dist/modules/processes/runner.js +4 -4
  209. package/dist/modules/providers/capabilities.js +28 -0
  210. package/dist/modules/providers/create.js +3 -4
  211. package/dist/modules/providers/factory.js +7 -7
  212. package/dist/modules/providers/fallback.js +12 -12
  213. package/dist/modules/providers/health.js +4 -4
  214. package/dist/modules/providers/manager.js +50 -19
  215. package/dist/modules/providers/naming.js +15 -0
  216. package/dist/modules/providers/preflight.js +71 -0
  217. package/dist/modules/providers/presets.js +39 -8
  218. package/dist/modules/providers/registry.js +7 -2
  219. package/dist/modules/reasoning/policy.js +8 -6
  220. package/dist/modules/reasoning/probe.js +30 -25
  221. package/dist/modules/security/audit-log.js +19 -36
  222. package/dist/modules/security/audit-notifier.js +35 -75
  223. package/dist/modules/security/command-validator.js +99 -51
  224. package/dist/modules/security/content-scanner.js +8 -8
  225. package/dist/modules/security/data-sanitizer.js +9 -16
  226. package/dist/modules/security/encryption.js +32 -52
  227. package/dist/modules/security/index.js +2 -1
  228. package/dist/modules/security/network-validator.js +120 -43
  229. package/dist/modules/security/path-validator.js +61 -46
  230. package/dist/modules/security/protected-paths.js +73 -0
  231. package/dist/modules/security/rate-limiter.js +17 -49
  232. package/dist/modules/security/security-policies.js +25 -57
  233. package/dist/modules/security/session-encryption.js +32 -71
  234. package/dist/modules/security/session-isolation.js +15 -28
  235. package/dist/modules/session/auto-name.js +63 -0
  236. package/dist/modules/session/export.js +98 -0
  237. package/dist/modules/session/index.js +1 -0
  238. package/dist/modules/session/manager.js +79 -28
  239. package/dist/modules/session/module.js +4 -3
  240. package/dist/modules/session/store.js +57 -20
  241. package/dist/modules/setup/answers.js +35 -0
  242. package/dist/modules/setup/apply.js +79 -0
  243. package/dist/modules/setup/discovery.js +72 -0
  244. package/dist/modules/skills/index.js +1 -0
  245. package/dist/modules/skills/loader.js +9 -1
  246. package/dist/modules/skills/module.js +11 -0
  247. package/dist/modules/updater/changelog-reader.js +15 -16
  248. package/dist/modules/updater/checker.js +9 -4
  249. package/dist/modules/updater/dev-detect.js +7 -6
  250. package/dist/modules/updater/index.js +1 -0
  251. package/dist/modules/updater/module.js +17 -15
  252. package/dist/modules/user-profile/compressor.js +2 -0
  253. package/dist/modules/user-profile/index.js +1 -0
  254. package/dist/modules/user-profile/profile.js +10 -1
  255. package/dist/modules/webui/answer-api.js +29 -0
  256. package/dist/modules/webui/assets.js +50 -0
  257. package/dist/modules/webui/body.js +21 -0
  258. package/dist/modules/webui/commands-settings.js +209 -0
  259. package/dist/modules/webui/commands-shared.js +60 -0
  260. package/dist/modules/webui/commands.js +174 -0
  261. package/dist/modules/webui/config-api.js +134 -0
  262. package/dist/modules/webui/file-api.js +202 -0
  263. package/dist/modules/webui/forward.js +159 -0
  264. package/dist/modules/webui/history-api.js +112 -0
  265. package/dist/modules/webui/host-state.js +60 -0
  266. package/dist/modules/webui/i18n-api.js +9 -0
  267. package/dist/modules/webui/limits.js +6 -0
  268. package/dist/modules/webui/log-forward.js +20 -0
  269. package/dist/modules/webui/memory-commands.js +64 -0
  270. package/dist/modules/webui/model-cache.js +18 -0
  271. package/dist/modules/webui/module.js +148 -0
  272. package/dist/modules/webui/persist.js +24 -0
  273. package/dist/modules/webui/prompt-io.js +97 -0
  274. package/dist/modules/webui/providers-api.js +337 -0
  275. package/dist/modules/webui/providers-view.js +56 -0
  276. package/dist/modules/webui/reasoning-view.js +15 -0
  277. package/dist/modules/webui/run-agent.js +81 -0
  278. package/dist/modules/webui/server.js +284 -0
  279. package/dist/modules/webui/sessions-api.js +178 -0
  280. package/dist/modules/webui/settings-api.js +561 -0
  281. package/dist/modules/webui/setup-api.js +201 -0
  282. package/dist/modules/webui/state.js +74 -0
  283. package/dist/modules/webui/util.js +261 -0
  284. package/dist/output/bus.js +7 -2
  285. package/dist/output/channel.js +80 -28
  286. package/dist/output/format.js +2 -1
  287. package/dist/output/index.js +1 -0
  288. package/dist/output/json-sink.js +5 -3
  289. package/dist/output/machine.js +4 -3
  290. package/dist/output/session-sink.js +4 -4
  291. package/dist/tools/approve.js +4 -0
  292. package/dist/tools/attach-image.js +5 -4
  293. package/dist/tools/auto-fixer.js +26 -44
  294. package/dist/tools/auto-tags.js +83 -0
  295. package/dist/tools/bash.js +193 -116
  296. package/dist/tools/browser.js +76 -31
  297. package/dist/tools/chunk-query.js +14 -6
  298. package/dist/tools/create-dir.js +9 -5
  299. package/dist/tools/delete-file.js +9 -5
  300. package/dist/tools/download-file.js +32 -7
  301. package/dist/tools/edit-file.js +52 -14
  302. package/dist/tools/enable-tools.js +6 -6
  303. package/dist/tools/executor.js +132 -27
  304. package/dist/tools/file-info.js +3 -1
  305. package/dist/tools/filter-tools.js +3 -3
  306. package/dist/tools/glob-tool.js +4 -1
  307. package/dist/tools/grep-tool.js +13 -10
  308. package/dist/tools/hidden-tools-block.js +6 -6
  309. package/dist/tools/index.js +16 -8
  310. package/dist/tools/list-dir.js +4 -2
  311. package/dist/tools/load-skill.js +7 -0
  312. package/dist/tools/mcp-call.js +15 -4
  313. package/dist/tools/meet-done.js +14 -0
  314. package/dist/tools/move-file.js +14 -7
  315. package/dist/tools/path-utils.js +29 -29
  316. package/dist/tools/pipeline-run.js +29 -7
  317. package/dist/tools/preview.js +1 -1
  318. package/dist/tools/process-ids.js +31 -0
  319. package/dist/tools/process-kill.js +9 -16
  320. package/dist/tools/process-list.js +1 -0
  321. package/dist/tools/process-log.js +11 -16
  322. package/dist/tools/process-wait.js +63 -0
  323. package/dist/tools/prompt-io.js +4 -3
  324. package/dist/tools/question.js +172 -25
  325. package/dist/tools/read-file.js +55 -34
  326. package/dist/tools/recall.js +14 -8
  327. package/dist/tools/registry.js +6 -5
  328. package/dist/tools/remember.js +8 -4
  329. package/dist/tools/scope-request.js +11 -6
  330. package/dist/tools/search-history.js +5 -2
  331. package/dist/tools/session-info.js +2 -0
  332. package/dist/tools/set-thinking.js +8 -4
  333. package/dist/tools/subagent.js +59 -39
  334. package/dist/tools/syntax-validator.js +292 -90
  335. package/dist/tools/user-input.js +22 -18
  336. package/dist/tools/web-browse.js +9 -3
  337. package/dist/tools/web-fetch.js +12 -23
  338. package/dist/tools/web-search.js +7 -5
  339. package/dist/tools/write-file.js +45 -15
  340. package/dist/ui/action-log.js +13 -0
  341. package/dist/ui/box.js +4 -3
  342. package/dist/ui/colors.js +2 -0
  343. package/dist/ui/diff.js +6 -5
  344. package/dist/ui/index.js +1 -0
  345. package/dist/ui/line-editor.js +230 -122
  346. package/dist/ui/line-math.js +10 -2
  347. package/dist/ui/md-formatter.js +57 -37
  348. package/dist/ui/output.js +8 -3
  349. package/dist/ui/plan-view.js +21 -10
  350. package/dist/ui/renderer.js +393 -67
  351. package/dist/ui/spinner.js +16 -5
  352. package/dist/ui/table.js +15 -17
  353. package/dist/ui/theme.js +143 -0
  354. package/dist/utils/child-run.js +20 -0
  355. package/dist/utils/error.js +1 -1
  356. package/dist/utils/index.js +2 -0
  357. package/dist/utils/retry.js +2 -2
  358. package/dist/utils/truncate.js +1 -1
  359. package/package.json +1 -1
@@ -6,41 +6,43 @@ import { processRegistry, registerKillable, unregisterKillable } from "../module
6
6
  import { t } from "../i18n/index";
7
7
  import { platform } from "os";
8
8
  import { MAX_PREVIEW_LINES } from "./preview";
9
+ import { BASH_GRACE_MS as CANONICAL_BASH_GRACE_MS,
10
+ /** Короткое окно для явного `background: true` — ловит ошибки спавна. */
11
+ BASH_SPAWN_SETTLE_MS as SPAWN_SETTLE_MS, } from "../config/timeouts";
9
12
  /**
10
- * A command still running after this window is promoted to the background.
11
- * The decision is based on process *behavior* (still alive), not on matching
12
- * words in the command text — a command that happens to mention "vite",
13
- * "server", etc. runs normally, and any genuinely long-running command is
14
- * caught regardless of how it is written.
13
+ * Команда, живая после окна ожидания, уходит в фон. Решение — по поведению
14
+ * процесса (всё ещё жив), а не по словам в тексте: упоминание "vite"/"server"
15
+ * выполняется обычно, а любая реально долгая команда ловится как ни пиши.
15
16
  */
16
- export const BASH_GRACE_MS = 5000;
17
- /** Short window for explicit `background: true` — surfaces immediate spawn failures (bad cwd, missing shell). */
18
- const SPAWN_SETTLE_MS = 100;
19
- /** Number of already-emitted lines shown in the background start message. */
17
+ /** Публичный экспорт-алиас: то же значение из реестра (используют тесты). */
18
+ export const BASH_GRACE_MS = CANONICAL_BASH_GRACE_MS;
19
+ /** Строк уже выданного вывода в стартовом сообщении фона. */
20
20
  const BG_OUTPUT_PREVIEW_LINES = 15;
21
21
  let bashGraceMs = BASH_GRACE_MS;
22
- /** Test hook: override the auto-background grace window. */
22
+ /** Тестовый хук: переопределить окно автобэкграунда. */
23
23
  export function setBashGraceMs(ms) {
24
24
  bashGraceMs = ms;
25
25
  }
26
26
  /**
27
- * Consecutive failures per first command word (resets on success). A model
28
- * that keeps calling a forbidden Unix command in cmd.exe (head/cat/grep/...)
29
- * gets a hint the first couple of times; after HARD_BLOCK_THRESHOLD consecutive
30
- * failures the tool hard-stops with a STOP directive instead of repeating the
31
- * same hint forever (observed: `head` called 5×, `Select-String` 3× in one run).
27
+ * Подряд идущие падения по первому слову команды (сброс при успехе). Модель,
28
+ * долбящую запрещённый Unix в cmd.exe (head/cat/grep/…), первые разы
29
+ * подсказываем; после HARD_BLOCK_THRESHOLD подряд — жёсткий стоп с STOP
30
+ * вместо вечного повтора хинта (наблюдено: `head` 5×, `Select-String` 3×).
32
31
  */
33
32
  const FAILING_FIRST_WORDS = new Map();
34
33
  const HARD_BLOCK_THRESHOLD = 3;
35
- /** Test hook: clear the consecutive-failure tracking state. */
34
+ // Module-level Map живёт весь процесс: при смене сессии (REPL /new, /resume)
35
+ // сбрасываем счётчики, чтобы старые падения не влияли на новую задачу.
36
+ let lastFailureSessionId;
37
+ /** Тестовый хук: сбросить счётчики подряд-падений. */
36
38
  export function resetBashFailureTracking() {
37
39
  FAILING_FIRST_WORDS.clear();
38
40
  }
39
41
  /**
40
- * Detect a file write via `echo/printf ... > file` — common model habit that
41
- * breaks in cmd.exe: single quotes are not grouping quotes, `>` only applies
42
- * to the LAST line of a multi-line command, and double quotes inside the text
43
- * split the command. Returns the target filename or null.
42
+ * Детект записи файла через `echo/printf ... > file` — частая привычка модели,
43
+ * ломающаяся в cmd.exe: одиночные кавычки не группируют, `>` относится только
44
+ * к ПОСЛЕДНЕЙ строке многострочной команды, двойные кавычки внутри рвут
45
+ * команду. Возвращает целевой файл или null.
44
46
  */
45
47
  export function extractEchoFileWrite(command) {
46
48
  if (!/^\s*(?:echo|printf)\b/i.test(command))
@@ -51,11 +53,10 @@ export function extractEchoFileWrite(command) {
51
53
  return m[1].replace(/["'']$/g, "");
52
54
  }
53
55
  /**
54
- * cmd.exe uses `&` as the command separator, not `;` (bash). The model
55
- * regularly chains commands with `;` — without translation cmd passes the
56
- * `;` to the first command as an argument (e.g. `node x.ts;` → ENOENT for
57
- * "x.ts;"). Replace `;` with `&` only OUTSIDE double-quoted strings so
58
- * `echo "a;b"` stays intact. (Single quotes are not special in cmd.)
56
+ * В cmd.exe разделитель — `&`, а не `;` (bash). Модель регулярно чейнит через
57
+ * `;` — без трансляции cmd отдаёт `;` первым аргументом команды (напр.
58
+ * `node x.ts;` → ENOENT для "x.ts;"). `;` → `&` только ВНЕ двойных кавычек,
59
+ * чтобы `echo "a;b"` уцелел (одиночные в cmd не спецсимвол).
59
60
  */
60
61
  export function translateSemicolonsForCmd(command) {
61
62
  let out = "";
@@ -74,13 +75,12 @@ export function translateSemicolonsForCmd(command) {
74
75
  function adaptCommandForWindows(command) {
75
76
  if (platform() !== "win32")
76
77
  return command;
77
- // The model sometimes appends `|| true` (bash error-suppression idiom)
78
- // which PowerShell doesn't understand. Replace with `; exit 0` which
79
- // forces a successful exit code regardless of the previous command's result.
78
+ // Модель иногда дописывает `|| true` (bash-идиома гашения ошибок), которой
79
+ // PowerShell не знает. Замена на `; exit 0` — успешный код при любом исходе.
80
80
  if (/\|\|\s*true\b/.test(command)) {
81
81
  command = command.replace(/\s*\|\|\s*true\b/g, "; exit 0");
82
82
  }
83
- // Windows mkdir does not support -p/--parents flag, but creates intermediate dirs by default
83
+ // Windows mkdir не знает -p/--parents, но промежуточные dirs создаёт по умолчанию.
84
84
  const trimmed = command.trim();
85
85
  if (/^mkdir\s+(-p|--parents)\b/.test(trimmed)) {
86
86
  return trimmed.replace(/^mkdir\s+(-p|--parents)\s*/, "mkdir ");
@@ -88,9 +88,9 @@ function adaptCommandForWindows(command) {
88
88
  if (/^mkdir\s+(-p|--parents)\s*$/.test(trimmed)) {
89
89
  return "mkdir";
90
90
  }
91
- // Translate simple Unix commands to their cmd.exe equivalents. Only the
92
- // leading word is rewritten; flags are passed through (ls -la → dir -la,
93
- // which cmd tolerates). Pipe-using forms are left alone — they would break.
91
+ // Трансляция простых Unix-команд в эквиваленты cmd.exe. Переписывается только
92
+ // ведущее слово; флаги сквозняком (ls -la → dir -la, cmd терпит). Формы с
93
+ // пайпом не трогаем — сломаются.
94
94
  const firstWord = trimmed.split(/\s+/)[0]?.split(/[\\/]/).pop();
95
95
  const translated = firstWord ? UNIX_TO_WIN_TRANSLATE[firstWord] : undefined;
96
96
  if (translated &&
@@ -100,10 +100,10 @@ function adaptCommandForWindows(command) {
100
100
  !trimmed.includes(";")) {
101
101
  return trimmed.replace(firstWord, translated);
102
102
  }
103
- // No encoding adaptation needed — registry.ts handles UTF-8/OEM decoding
103
+ // Адаптации кодировок не надо — UTF-8/OEM-декод в registry.ts.
104
104
  return translateSemicolonsForCmd(command);
105
105
  }
106
- /** Common Unix → Windows command mapping for error hints. */
106
+ /** Типовой маппинг Unix → Windows для хинтов об ошибках. */
107
107
  const UNIX_TO_WIN_HINTS = {
108
108
  ls: "Use the list_dir tool instead.",
109
109
  pwd: "Use the file_info tool instead.",
@@ -142,25 +142,23 @@ const UNIX_TO_WIN_HINTS = {
142
142
  "Get-ChildItem": "Use the list_dir tool instead.",
143
143
  "Remove-Item": "Use the delete_file tool instead.",
144
144
  };
145
- /** Unix commands that can be transparently translated to cmd.exe equivalents. */
145
+ /** Unix-команды, прозрачно транслируемые в эквиваленты cmd.exe. */
146
146
  const UNIX_TO_WIN_TRANSLATE = {
147
147
  ls: "dir",
148
148
  pwd: "cd",
149
149
  cat: "type",
150
150
  };
151
151
  /**
152
- * Detect when the model mistakes a tool call for a shell command — e.g.
153
- * `bash` with command "create_dir path=C:\...\src" or "read_file file=x".
154
- * These are tool invocations, not commands; running them through the shell
155
- * fails. The matched tool name and raw args are returned so the caller can
156
- * redirect into the real tool.
152
+ * Детект перепутанного tool call в shell-команде — напр. `bash` с командой
153
+ * "create_dir path=C:\...\src" или "read_file file=x". Это вызовы тулов, а не
154
+ * команды; прогон через шелл падает. Возвращает имя тула и сырые аргументы
155
+ * для редиректа в настоящий тул.
157
156
  */
158
157
  /**
159
- * Shell commands that must NEVER be treated as mistaken tool calls. The
160
- * model often writes files via `echo '<code with = signs>'` or reads them
161
- * via `cat` — those are shell commands, not tool invocations. Without this
162
- * guard the redirect heuristic fires on any `<word> <text containing =>`
163
- * and fails with "Unknown tool: echo".
158
+ * Shell-команды, которые НИКОГДА не считать перепутанными tool call. Модель
159
+ * часто пишет файлы через `echo '<код со знаками =>` или читает через `cat` —
160
+ * это команды шелла. Без гарда эвристика редиректа срабатывает на любое
161
+ * `<слово> <текст с =>` и падает с "Unknown tool: echo".
164
162
  */
165
163
  const NEVER_TOOL_CALLS = new Set([
166
164
  "echo",
@@ -185,17 +183,18 @@ const NEVER_TOOL_CALLS = new Set([
185
183
  "sed",
186
184
  "awk",
187
185
  ]);
186
+ /** Определяет перепутанный tool call в команде bash. Возврат: тул и сырые аргументы. */
188
187
  export function detectToolCallInBash(command) {
189
188
  const match = command.trim().match(/^([\w-]+)\s+(.+)$/s);
190
189
  if (!match)
191
190
  return null;
192
191
  const tool = match[1];
193
- // Real shell commands (echo, cat, ...) are never mistaken tool calls.
192
+ // Настоящие shell-команды (echo, cat, …) — никогда не перепутанные tool call.
194
193
  if (NEVER_TOOL_CALLS.has(tool))
195
194
  return null;
196
195
  const rest = match[2].trim();
197
- // Only treat as a tool call when the first word looks like a snake_case
198
- // tool name and the rest has at least one '=' or a JSON object shape.
196
+ // Tool call — только если первое слово похоже на snake_case-имя тула, а
197
+ // остаток содержит хотя бы один '=' или JSON-форму.
199
198
  if (!/^[a-z][a-z0-9_]+$/.test(tool))
200
199
  return null;
201
200
  if (!rest.includes("=") && !rest.startsWith("{"))
@@ -203,9 +202,9 @@ export function detectToolCallInBash(command) {
203
202
  return { tool, args: rest };
204
203
  }
205
204
  /**
206
- * Parse the raw argument string of a mistaken tool call captured in a bash
207
- * command into a Record. Supports JSON objects ("{"path": "..."}") and
208
- * key=value pairs ("path=C:\...\src"). Values keep their literal text.
205
+ * Парсит сырую строку аргументов перепутанного tool call в Record. Понимает
206
+ * JSON-объекты ("{"path": "..."}") и пары key=value ("path=C:\...\src").
207
+ * Значения — дословным текстом. Возврат: словарь аргументов.
209
208
  */
210
209
  export function parseToolArgs(raw) {
211
210
  const trimmed = raw.trim();
@@ -218,29 +217,28 @@ export function parseToolArgs(raw) {
218
217
  }
219
218
  }
220
219
  const args = {};
221
- // Tokenize respecting double/single-quoted values.
220
+ // Токенизация с уважением к кавычкам в значениях.
222
221
  const tokens = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
223
222
  for (const token of tokens) {
224
223
  const eq = token.indexOf("=");
225
224
  if (eq > 0) {
226
225
  const key = token.slice(0, eq);
227
226
  const value = token.slice(eq + 1);
228
- // Strip surrounding quotes from values.
227
+ // Окаймляющие кавычки значений — снять.
229
228
  args[key] = value.replace(/^["']|["']$/g, "");
230
229
  }
231
230
  }
232
231
  return args;
233
232
  }
234
233
  /**
235
- * Detect a test-runner report embedded in command output (bun test / vitest /
236
- * jest / mocha / `go test` / pytest). Returns null when the output does not
237
- * look like a test run. Used to inject a warning when tests fail so the model
238
- * cannot close a "run the tests" step as done.
234
+ * Детект отчёта тест-раннера в выводе (bun test/vitest/jest/mocha/`go test`/
235
+ * pytest). Не похоже на тесты — null. Нужно, чтобы вставить warning при
236
+ * падениях и модель не закрыла шаг "прогнать тесты" как done.
239
237
  */
240
238
  export function detectTestResults(output) {
241
239
  if (!output)
242
240
  return null;
243
- // Framework-specific quantifiers first (unambiguous).
241
+ // Сначала однозначные фреймворк-специфичные счётчики.
244
242
  const jest = output.match(/Tests:\s+(\d+)\s+passed,\s*(\d+)\s+failed/i);
245
243
  if (jest) {
246
244
  return { framework: "jest", passed: +jest[1], failed: +jest[2] };
@@ -265,7 +263,7 @@ export function detectTestResults(output) {
265
263
  failed: pytest[2] ? +pytest[2] : 0,
266
264
  };
267
265
  }
268
- // bun test / vitest: per-case markers + trailing "N pass / M fail".
266
+ // bun test / vitest: покейсные маркеры + хвостовое "N pass / M fail".
269
267
  const failMarkers = output.match(/\(fail\)/g)?.length ?? 0;
270
268
  const passMarkers = output.match(/\(pass\)/g)?.length ?? 0;
271
269
  const xMarkers = output.match(/\s×\s/g)?.length ?? 0;
@@ -285,17 +283,17 @@ export function detectTestResults(output) {
285
283
  return null;
286
284
  }
287
285
  /**
288
- * R5: a command that runs a script file (bun/node/python/...) exited 0 but
289
- * printed nothing. The most common cause in agent sessions: the program has
290
- * no entry point — nobody calls its main() with the CLI args. Surface a
291
- * concrete hint instead of letting the model stare at an empty output.
286
+ * R5: команда запуска скрипта (bun/node/python/…) вышла в 0, но молча.
287
+ * Частая причина в агент-сессиях: нет точки входа — main() с CLI-аргументами
288
+ * никто не вызывает. Конкретный хинт вместо молчаливого пустого вывода.
292
289
  */
293
290
  const CLI_FILE_RUN_RE = /\b(bun|node|deno|python|python3|tsx|ts-node|php|ruby|go\s+run)\S*\s+(run\s+)?["']?[\w./\\-]+\.(ts|js|tsx|jsx|mjs|cjs|py)\b/;
291
+ /** Хинт на пустой прогон CLI-файла (код 0, stdout пуст). */
294
292
  export function emptyCliRunHint(command, output, code) {
295
293
  if (code !== 0 || output.trim())
296
294
  return null;
297
295
  if (/[>|]/.test(command))
298
- return null; // redirection/pipes — output went elsewhere
296
+ return null; // редирект/пайп — вывод ушёл мимо
299
297
  if (/\b(bun test|vitest|pytest|jest|mocha|--test)\b/i.test(command))
300
298
  return null;
301
299
  if (!CLI_FILE_RUN_RE.test(command))
@@ -303,22 +301,83 @@ export function emptyCliRunHint(command, output, code) {
303
301
  return "the command exited 0 but printed NOTHING to stdout. If this should run a CLI program, the file probably has no entry point: read it with read_file and check the code actually calls its main function with command-line arguments (e.g. main(process.argv[2])) and prints results with console.log.";
304
302
  }
305
303
  /**
306
- * Detect npm/npx/bunx failing with "could not determine executable to run"
307
- * — the package declares no `bin` (or `npm run <script>` named a missing
308
- * script). Append a concrete hint instead of letting the model re-try blindly
309
- * (observed in two agent sessions: the model burned many iterations re-running
310
- * the same failing `npx`/`bunx`/`npm run` command).
304
+ * Детект npm/npx/bunx с "could not determine executable to run" — у пакета
305
+ * нет `bin` (или `npm run <script>` назвал несуществующий скрипт). Конкретный
306
+ * хинт вместо слепых ретраев (наблюдено в двух сессиях: модель жгла итерации
307
+ * на повторах той же падающей команды).
311
308
  */
312
309
  const NPM_EXEC_RE = /could not determine executable to run/i;
313
310
  /**
314
- * A command that waited for interactive input with no TTY aborts with
315
- * "Operation cancelled" (Bun/Node prompt) or "stdin is not a TTY". Common on
316
- * bare `bun create`/`npm create` scaffolds. Append a concrete non-interactive
317
- * remedy instead of letting the model retry the identical interactive command.
311
+ * Команда ждала интерактивного ввода без TTY и абортнулась ("Operation
312
+ * cancelled" у Bun/Node или "stdin is not a TTY"). Типично для голых
313
+ * `bun create`/`npm create`. Конкретное неинтерактивное лечение вместо
314
+ * повтора той же команды.
318
315
  */
319
316
  const INTERACTIVE_PROMPT_RE = /operation (?:cancelled|canceled|aborted)|stdin is not a tty|not a tty|cannot prompt without a tty/i;
320
- /** process-management tools hidden behind enable_tools (tag "shell"). */
321
- const HIDDEN_PROCESS_TOOLS = ["process_list", "process_log", "process_kill"];
317
+ /** Тулы управления процессами за enable_tools (тег "shell"). */
318
+ const HIDDEN_PROCESS_TOOLS = ["process_list", "process_log", "process_kill", "process_wait"];
319
+ /**
320
+ * Read-only префиксы для мемоизации (см. `manualMemo` тула). Эвристика,
321
+ * намеренно узкая: одиночные вызовы, немутирующие по построению. Всё составное
322
+ * (`&`, `|`, `<`, `>`, `;`) — никогда в мемо. TTL 120с в исполнителе
323
+ * ограничивает протухание от внешних правок.
324
+ */
325
+ const READONLY_COMMAND_PREFIXES = [
326
+ "git status",
327
+ "git log",
328
+ "git diff",
329
+ "git branch",
330
+ "git tag",
331
+ "git rev-parse",
332
+ "git ls-remote",
333
+ "git remote",
334
+ "git show",
335
+ "npm view",
336
+ "npm list",
337
+ "npm config get",
338
+ "npm whoami",
339
+ "npm ping",
340
+ "npm --version",
341
+ "node --version",
342
+ "python --version",
343
+ "python3 --version",
344
+ "bun --version",
345
+ "git --version",
346
+ "dir",
347
+ "ls",
348
+ "echo",
349
+ "type",
350
+ "where",
351
+ "which",
352
+ "cat",
353
+ ];
354
+ const READONLY_COMMAND_PATTERNS = READONLY_COMMAND_PREFIXES.map((p) => new RegExp(`^${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`));
355
+ /**
356
+ * Мемоизируема ли (post-adaptation) команда: одиночный read-only вызов,
357
+ * чей успешный результат можно кешировать. Экспортировано для тестов.
358
+ */
359
+ export function isMemoizableCommand(command) {
360
+ const cmd = command.trim().toLowerCase();
361
+ if (!cmd || /[|<>;&]/.test(cmd))
362
+ return false;
363
+ return READONLY_COMMAND_PATTERNS.some((re) => re.test(cmd));
364
+ }
365
+ // Оборонительный доступ к мемо: самописные стабы ToolExecutor в тестах (и любой
366
+ // будущий минимальный исполнитель) могут не иметь memo-методов — считать промахом.
367
+ function memoRead(ctx, args) {
368
+ const get = ctx.toolExecutor?.memoGet;
369
+ return typeof get === "function" ? get.call(ctx.toolExecutor, "bash", args) : undefined;
370
+ }
371
+ function memoWrite(ctx, args, result) {
372
+ const set = ctx.toolExecutor?.memoSet;
373
+ if (typeof set === "function")
374
+ set.call(ctx.toolExecutor, "bash", args, result);
375
+ }
376
+ function memoClear(ctx) {
377
+ const clear = ctx.toolExecutor?.memoInvalidate;
378
+ if (typeof clear === "function")
379
+ clear.call(ctx.toolExecutor);
380
+ }
322
381
  /**
323
382
  * Разрешение bash-security-конфига: session-изоляция поверх глобального,
324
383
  * мастер-выключатель `enabled: false` гасит все под-модули.
@@ -340,15 +399,15 @@ function resolveBashSecurityConfig(ctx) {
340
399
  function decorateCommandOutput(opts) {
341
400
  let output = opts.output;
342
401
  const { ctx, command, originalCommand, code, testRun, workdir, securityConfig } = opts;
343
- // R5: script file ran with exit 0 but empty output — likely a missing
344
- // entry point (main is never called with argv).
402
+ // R5: скрипт вышел в 0 молча — вероятно, нет точки входа
403
+ // (main с argv никто не вызывает).
345
404
  const cliHint = emptyCliRunHint(command, output, code);
346
405
  if (cliHint) {
347
406
  output = `(exit code 0, no output)\n\nHint: ${cliHint}`;
348
407
  }
349
- // Auto-verify test runs: a failing suite must not be reported as a clean
350
- // success. Inject a prominent marker the model cannot miss (the typical
351
- // failure: tests fail but the CLI exit code is 0).
408
+ // Автоверификация тест-ранов: упавший сьют нельзя отдавать как чистый
409
+ // успех. Видный маркер, который модель не пропустит (типичный провал:
410
+ // тесты падают при нулевом exit code CLI).
352
411
  if (testRun && testRun.failed > 0) {
353
412
  output =
354
413
  t("exec.test_runner_fail", {
@@ -364,19 +423,21 @@ function decorateCommandOutput(opts) {
364
423
  if (!output && code !== 0) {
365
424
  output = `(exit code ${code})`;
366
425
  }
367
- // npm/npx/bunx: "could not determine executable to run" — the package has
368
- // no `bin` entry (or the script does not exist). Cross-platform, so it runs
369
- // before the win32 hint block.
426
+ // npm/npx/bunx: "could not determine executable to run" — у пакета нет
427
+ // записи `bin` (или скрипта нет). Кроссплатформенно — до win32-блока хинтов.
370
428
  output = npmExecHint(output);
371
- // Interactive prompt with no TTY (Bun/Node "Operation cancelled").
429
+ // Интерактивный промпт без TTY (Bun/Node "Operation cancelled").
372
430
  output = interactivePromptHint(output);
373
- // On Windows, hint about Unix commands that don't work, and hard-stop a
374
- // command that keeps failing the same way. The hint keys on the ORIGINAL
375
- // command word (before adaptCommandForWindows translated cat→type): the
376
- // translation is only for execution, but the model wrote `cat`, and that is
377
- // what UNIX_TO_WIN_HINTS knows about. Keying on the translated word left
378
- // `cat` invisible to the hint/block (observed: model ran cat 4+ times).
431
+ // На Windows — хинт про нерабочие Unix-команды и жёсткий стоп повторяющегося
432
+ // падения. Хинт ключуют по ОРИГИНАЛЬНОМУ первому слову (до трансляции
433
+ // adaptCommandForWindows cat→type): трансляция только для исполнения, а
434
+ // модель писала `cat` — его и знает UNIX_TO_WIN_HINTS. Ключ по транслированному
435
+ // слову делал `cat` невидимым для хинта/блока (наблюдено: cat 4+ раз).
379
436
  if (platform() === "win32") {
437
+ if (ctx.sessionId !== lastFailureSessionId) {
438
+ lastFailureSessionId = ctx.sessionId;
439
+ FAILING_FIRST_WORDS.clear();
440
+ }
380
441
  const originalFirstWord = originalCommand.trim().split(/\s+/)[0]?.split(/[\\/]/).pop();
381
442
  if (originalFirstWord && originalFirstWord in UNIX_TO_WIN_HINTS) {
382
443
  if (code === 0) {
@@ -394,15 +455,15 @@ function decorateCommandOutput(opts) {
394
455
  }
395
456
  }
396
457
  }
397
- // Update audit log with result
458
+ // Запись результата в аудит-лог.
398
459
  if (securityConfig?.logCommands) {
399
460
  logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), code === 0, `Working directory: ${workdir}, Output length: ${output.length}`);
400
461
  }
401
462
  const lines = output.split("\n");
402
463
  if (testRun && testRun.failed > 0) {
403
- // Keep the failure details: test runners print passing markers first and
404
- // the errors + summary at the very end. A short head preview hides exactly
405
- // what the model needs to fix the failures.
464
+ // Детали падения сохранить: раннеры печатают passing-маркеры первыми, а
465
+ // ошибки + итог — в самом конце. Короткий head-превью прячет ровно то,
466
+ // что нужно модели для починки.
406
467
  const TEST_TAIL_LINES = 400;
407
468
  const kept = lines.slice(-TEST_TAIL_LINES);
408
469
  const skipped = lines.length - kept.length;
@@ -417,13 +478,14 @@ function decorateCommandOutput(opts) {
417
478
  }
418
479
  return output;
419
480
  }
481
+ /** Дописывает хинт на npm-без-bin (по выводу). */
420
482
  export function npmExecHint(output) {
421
483
  if (NPM_EXEC_RE.test(output)) {
422
484
  return `${output}\n\nHint: ${t("exec.npm_exec_hint")}`;
423
485
  }
424
486
  return output;
425
487
  }
426
- /** Append a non-interactive remedy when a command aborted waiting on stdin. */
488
+ /** Дописывает неинтерактивное лечение при аборте на ожидании stdin. */
427
489
  export function interactivePromptHint(output) {
428
490
  if (INTERACTIVE_PROMPT_RE.test(output)) {
429
491
  return `${output}\n\nHint: ${t("exec.interactive_hint")}`;
@@ -433,7 +495,7 @@ export function interactivePromptHint(output) {
433
495
  export const bashTool = {
434
496
  name: "bash",
435
497
  icon: "💻",
436
- description: "Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Commands that are still running after a few seconds are automatically moved to the background and return a process id — manage them with process_list, process_log, process_kill. Set background=true to return a process id immediately for commands you know are long-running (dev servers, watchers).\n\nWindows notes: the shell is cmd.exe — PowerShell cmdlets (Write-Host, Get-Content, Select-String, Out-File) and bash heredocs (cat << EOF) do NOT work there. Use the read_file/write_file tools instead of cat/echo redirection. Sequential commands: use && (a leading ; is auto-converted to &).",
498
+ description: "Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Commands that are still running after a few seconds are automatically moved to the background and return a process id — manage them with process_list, process_log, process_wait, process_kill. For a finite background task prefer a single process_wait over polling process_log or sleeping (never use ping/timeout/sleep as a delay). Set background=true to return a process id immediately for commands you know are long-running (dev servers, watchers).\n\nWindows notes: the shell is cmd.exe — PowerShell cmdlets (Write-Host, Get-Content, Select-String, Out-File) and bash heredocs (cat << EOF) do NOT work there. Use the read_file/write_file tools instead of cat/echo redirection. Sequential commands: use && (a leading ; is auto-converted to &).",
437
499
  tags: ["shell", "code"],
438
500
  parameters: {
439
501
  type: "object",
@@ -450,16 +512,17 @@ export const bashTool = {
450
512
  },
451
513
  required: ["command"],
452
514
  },
515
+ /** Мемоизация покомандно (см. isMemoizableCommand) — ведётся здесь. */
516
+ manualMemo: true,
453
517
  handler: async (ctx, args) => {
454
518
  const originalCommand = String(args.command);
455
- // The model sometimes sends a tool invocation (e.g. "create_dir path=...")
456
- // as a bash command instead of calling the tool directly. Redirect into
457
- // the real tool so the intent succeeds instead of failing in the shell.
519
+ // Модель иногда шлёт вызов тула (напр. "create_dir path=…") командой bash
520
+ // вместо прямого вызова. Редирект в настоящий тул — намерение сработает,
521
+ // а не упадёт в шелле.
458
522
  const toolCall = detectToolCallInBash(originalCommand);
459
523
  if (toolCall &&
460
524
  toolCall.tool !== "bash" &&
461
- ctx.toolExecutor &&
462
- ctx.toolExecutor.hasTool(toolCall.tool)) {
525
+ ctx.toolExecutor?.hasTool(toolCall.tool)) {
463
526
  const redirected = await ctx.toolExecutor.executeByName(toolCall.tool, parseToolArgs(toolCall.args), ctx);
464
527
  return {
465
528
  success: redirected.success,
@@ -467,11 +530,9 @@ export const bashTool = {
467
530
  };
468
531
  }
469
532
  const command = adaptCommandForWindows(originalCommand);
470
- // Hidden-tool discovery (ses_mt4fn58c): with tool-set narrowing the
471
- // process_* tools are invisible to the model, so running them through
472
- // cmd.exe only yields "is not recognized" — three wasted attempts in
473
- // that session. Point the model at enable_tools. (k=v forms are handled
474
- // by the redirect above and reach the real tool.)
533
+ // Discovery скрытых тулов: при сужении набора process_* невидимы модели,
534
+ // и прогон их через cmd.exe даёт лишь "is not recognized" (в одной сессии —
535
+ // три впустую). Указать на enable_tools (k=v-формы ушли редиректом выше).
475
536
  const baseCmd = originalCommand.trim().split(/\s+/)[0];
476
537
  if (HIDDEN_PROCESS_TOOLS.includes(baseCmd) &&
477
538
  ctx.toolExecutor?.hasTool?.(baseCmd)) {
@@ -480,10 +541,9 @@ export const bashTool = {
480
541
  output: t("exec.hidden_tool_hint", { tool: baseCmd }),
481
542
  };
482
543
  }
483
- // echo/printf redirection to a file is unreliable in cmd.exe (single
484
- // quotes don't group, multi-line commands split, embedded double quotes
485
- // break the command). Steer the model to write_file instead — it
486
- // produces correct files every time.
544
+ // echo/printf-редирект в файл ненадёжен в cmd.exe (одиночные кавычки не
545
+ // группируют, многострочник рвётся, вложенные двойные кавычки ломают
546
+ // команду). Вести на write_file — с ним файлы корректны всегда.
487
547
  if (platform() === "win32") {
488
548
  const echoWrite = extractEchoFileWrite(originalCommand);
489
549
  if (echoWrite) {
@@ -498,28 +558,41 @@ export const bashTool = {
498
558
  const securityConfig = resolveBashSecurityConfig(ctx);
499
559
  const validation = isCommandAllowed(command, securityConfig);
500
560
  if (!validation.allowed) {
501
- // Log security block
502
561
  logSecurityBlock(ctx.sessionId, "bash_command", validation.reason || "Command blocked by security policy", sanitizeCommandForLog(originalCommand));
503
562
  return {
504
563
  success: false,
505
- output: `[SECURITY BLOCKED] Command is not allowed: ${validation.reason}\nHint: Use the "workdir" parameter to run commands in a specific directory instead of "cd dir && cmd". Run one command per tool call.`,
564
+ output: `[SECURITY BLOCKED] Command is not allowed: ${validation.reason}\nHint: run one plain command per tool call. For output redirection write the file with write_file; for chaining run separate calls; use the "workdir" parameter instead of "cd dir && cmd". Do NOT write a wrapper script (.bat/.cmd/.sh/.ps1) to bypass this — the same rule applies when that script runs.`,
506
565
  };
507
566
  }
508
- // Log command execution if enabled
567
+ // Лог запуска команды, если включён.
509
568
  if (securityConfig?.logCommands) {
510
569
  logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, // Will be updated after execution
511
570
  `Working directory: ${workdir}`);
512
571
  }
572
+ // Мемо результата (manualMemo, после security-гейта, чтобы блок никогда не
573
+ // маскировался stored-результатом): одиночные read-only отдаются из мемо
574
+ // исполнителя; всё остальное его сбрасывает. Фоновые запуски не мемоизируются
575
+ // (process id одноразовые).
576
+ const memoKey = { command, workdir };
577
+ const memoizable = args.background !== true && isMemoizableCommand(command);
578
+ if (memoizable) {
579
+ const hit = memoRead(ctx, memoKey);
580
+ if (hit)
581
+ return { ...hit };
582
+ }
583
+ else {
584
+ memoClear(ctx);
585
+ }
513
586
  const entry = processRegistry.start(command, workdir, ctx.sessionId);
514
587
  if (ctx.activeCallId) {
515
- // Let the executor abort/kill the child during the grace window.
588
+ // Исполнителю — убить дочерний процесс в grace-окне при аборте.
516
589
  registerKillable(ctx.activeCallId, () => processRegistry.kill(entry.id));
517
590
  }
518
591
  try {
519
592
  const settleMs = args.background === true ? SPAWN_SETTLE_MS : bashGraceMs;
520
593
  const exited = await processRegistry.waitForExit(entry.id, settleMs);
521
594
  if (exited) {
522
- // Command finished (or failed to spawn) within the window.
595
+ // Команда уложилась в окно (включая ошибку спавна).
523
596
  if (entry.spawnError) {
524
597
  return {
525
598
  success: false,
@@ -541,9 +614,13 @@ export const bashTool = {
541
614
  workdir,
542
615
  securityConfig,
543
616
  });
617
+ // Успешные read-only прогоны — в мемо (падения могут быть транзиентны).
618
+ if (memoizable && code === 0) {
619
+ memoWrite(ctx, memoKey, { success: true, output });
620
+ }
544
621
  return { success: code === 0, output };
545
622
  }
546
- // Still running — promote to a background process.
623
+ // Всё ещё жива — в фоновые процессы.
547
624
  const explicit = args.background === true;
548
625
  const preview = entry.log
549
626
  .filter((l) => l.trim() !== "")