micro-models-agent 2.8.0 → 2.9.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to Micro Models Agent (MMA) will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [2.9.0] - 2026-09-26
8
+
9
+ ### Added
10
+
11
+ - **`process_wait` tool** (`src/tools/bash.ts`, wired into `manage_hint`, `bash`/`process_log` descriptions and the hidden-tool redirect list): a single blocking wait on a background process (`id`/`timeoutMs`/`tail`) that returns the buffered tail when it exits — replaces `process_log` polling loops and `ping`/`sleep` delays.
12
+ - **Result memoization** (`src/tools/executor.ts`, `src/tools/bash.ts`): read-only tools (`read_file`/`list_dir`/`glob`/`grep`/`file_info`) plus single, read-only bash commands are cached per session (120s TTL), wiped by any mutation. Repeats of the same read no longer re-execute.
13
+ - **Success-claim check** (`src/modules/hallucination/success-claim.ts`, wired into `src/core/agent/hallucination-gate.ts`): a strong completion claim ("done"/"published") together with error signals in the last 10 tool outputs and no hedging triggers one bounded re-check (`MAX_CLAIM_CHECK_RETRIES = 1`, `hall.claim_contradiction`), then fails open — never a block. Deterministic (regex + tool success flags, no LLM), Cyrillic-aware.
14
+ - **`session export --compact`** (`src/cli/commands.ts`, `src/modules/session/export.ts`): human-readable export without tool-result dumps.
15
+
16
+ ### Fixed
17
+
18
+ - **repeats made visible**: `getStuckReason` now surfaces repetitive calls (previously a warning key with no logged reason); `process_log` poll loops point at `process_wait`; the quiet renderer folds consecutive identical action lines into a single `xN` line.
19
+ - **web think-toggle alignment** (`src/modules/webui/assets/styles.css`, web `action.hide/show` i18n): dropped the `▾`/`▸` glyphs from the toggle labels in favor of an inline-flex SVG slot, fixing the alignment.
20
+
7
21
  ## [2.8.0] - 2026-09-26
8
22
 
9
23
  ### Added
@@ -239,6 +239,8 @@ body {
239
239
  .ans { color: var(--ans); white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }
240
240
  .think-h { color: var(--think); cursor: pointer; user-select: none; display: flex; gap: 8px; align-items: baseline; font-size: 12.5px; }
241
241
  .think-h .dur, .think-h .tog { color: var(--dim-b); }
242
+ .think-h .tog { display: inline-flex; align-items: center; gap: 4px; }
243
+ .think-h .tog .svg-icon { width: 12px; height: 12px; }
242
244
  .think-h .tog:hover { color: var(--dim-a); }
243
245
  .think-body { color: var(--dim-c); padding-left: 18px; white-space: pre-wrap; font-size: 12.5px; overflow-wrap: anywhere; }
244
246
  .think-body.hidden { display: none; }
package/dist/i18n/en.json CHANGED
@@ -200,7 +200,7 @@
200
200
  "tool.interactive_disabled": "Interactive tool is disabled in exit-on-complete mode. Proceed without asking the user.",
201
201
  "proc.started": "Started background process {id} (PID {pid}).\nCommand: {command}",
202
202
  "proc.promoted_hint": "Command still running after {ms} ms — moved to the background",
203
- "proc.manage_hint": "Check output: process_log id={id}. Stop it: process_kill id={id}. List all: process_list.",
203
+ "proc.manage_hint": "Check output: process_log id={id}. Wait for completion: process_wait id={id}. Stop it: process_kill id={id}. List all: process_list.",
204
204
  "proc.output_preview": "First output:\n{lines}",
205
205
  "proc.none": "No background processes running.",
206
206
  "proc.not_found": "Process not found: {id}",
@@ -215,9 +215,13 @@
215
215
  "proc.status_running": "running",
216
216
  "proc.status_exited": "exited",
217
217
  "proc.status_killed": "killed",
218
+ "proc.wait_exited": "Process {id} {status} after {ms} ms with exit code {code}.",
219
+ "proc.wait_running": "Process {id} still running after {ms} ms.",
220
+ "proc.wait_hint": "Wait again: process_wait id={id}. Check output: process_log id={id}. Stop it: process_kill id={id}.",
218
221
  "tool.friendly.process_list": "Listing background processes",
219
222
  "tool.friendly.process_log": "Process output",
220
223
  "tool.friendly.process_kill": "Stopping process",
224
+ "tool.friendly.process_wait": "Waiting for process",
221
225
  "plan.no_steps": "No plan steps specified. Provide concrete steps with files and commands.",
222
226
  "plan.created": "Plan created: {title} ({steps} steps)",
223
227
  "plan.coverage_warning": "Plan may be missing required files from the task: {missing}. Add steps covering them.",
@@ -419,6 +423,7 @@
419
423
  "cli.export_format": "Output format: md|json|jsonl (default md)",
420
424
  "cli.export_out": "Output path ('-' for stdout)",
421
425
  "cli.export_no_redact": "Do not redact secrets",
426
+ "cli.export_compact": "Omit tool result bodies (compact readable export)",
422
427
  "cli.first_run": "First run detected. Running setup wizard...",
423
428
  "cli.first_run_oneshot": "No configuration found — running with defaults. Run `mma` (no arguments) once to set up the wizard.",
424
429
  "cli.web_setup_required": "No configuration found — opening the browser setup wizard.",
@@ -623,7 +628,7 @@
623
628
  "repl.help_aliases": "Aliases: {names}",
624
629
  "repl.help_examples": "Examples:",
625
630
  "repl.export": "Export a session to a file",
626
- "repl.export_usage": "Usage: /export [<id|name>] [--format md|json|jsonl] [--out <path>] [--no-redact]",
631
+ "repl.export_usage": "Usage: /export [<id|name>] [--format md|json|jsonl] [--out <path>] [--no-redact] [--compact]",
627
632
  "config.header": "Managed settings",
628
633
  "config.col_setting": "Setting",
629
634
  "config.col_value": "Value",
@@ -751,6 +756,7 @@
751
756
  "exec.tool_errors": "Tool {tool} failed {count} times. Consider using a different tool.",
752
757
  "exec.tool_errors_recovery": "Tool {tool} has failed {count} times in a row. Try an alternative: create files directly via write_file, use a different command, or if nothing works — skip this step via plan update step=N status=skipped with a note explaining why.",
753
758
  "exec.repetitive_tool": "Called {tool} {count} times with identical arguments and result. Try a different approach — create files directly, change arguments, or check process status via process_log.",
759
+ "exec.repetitive_process_log_hint": "You are polling a background process — call process_wait once instead of polling process_log.",
754
760
  "exec.consecutive_failures_recovery": "{count} consecutive tool failures. Create files directly via write_file instead of terminal commands. Check that dependencies are installed (npm install). Do not run build/tests until all files are created.",
755
761
  "exec.bash_flailing_recovery": "bash failed in {fails} of the last {window} attempts. Stop using shell commands for file operations on this machine — they do not work. Use the dedicated tools instead: read_file (the header shows line counts), write_file, edit_file, file_info (file size), glob/grep for search",
756
762
  "exec.bash_flailing": "bash failed in {fails} of the last {window} attempts.",
@@ -793,6 +799,7 @@
793
799
  "hall.uncertainty": "Uncertainty markers: {markers}",
794
800
  "hall.unknown_paths": "Mentioned file paths not found in known files: {paths}",
795
801
  "hall.contradiction_llm": "Contradicts an earlier decision{reason}",
802
+ "hall.claim_contradiction": "The answer claims success, but a recent tool result indicates a problem ({tool}: {snippet}). Either verify and fix, then re-run the check — or soften the claim to match the evidence (e.g. staged, will appear shortly).",
796
803
  "hall.uncertainty_prefix": "\n\n[⚠️ Uncertainty] ",
797
804
  "browser.repeated_action": "You have repeated the same action ({action}) {threshold} times. Try a different approach: use \"snapshot\" to re-read the page, try different element numbers, or navigate to a different URL.",
798
805
  "browser.unknown_action": "Unknown action: {action}",
@@ -1218,8 +1225,8 @@
1218
1225
  "web.selectors.current": "current",
1219
1226
  "web.selectors.context_prompt": "context window (tokens)",
1220
1227
  "web.action.thinking": "thinking",
1221
- "web.action.hide": "▾ hide",
1222
- "web.action.show": "▸ show",
1228
+ "web.action.hide": "hide",
1229
+ "web.action.show": "show",
1223
1230
  "web.action.actions_one": "{count} action",
1224
1231
  "web.action.actions_few": "{count} actions",
1225
1232
  "web.action.actions_many": "{count} actions",
package/dist/i18n/ru.json CHANGED
@@ -198,7 +198,7 @@
198
198
  "tool.interactive_disabled": "Интерактивный инструмент отключён в режиме exit-on-complete. Продолжай без вопроса пользователю.",
199
199
  "proc.started": "Фоновый процесс запущен: {id} (PID {pid}).\nКоманда: {command}",
200
200
  "proc.promoted_hint": "Команда всё ещё выполняется через {ms} мс — переведена в фоновый режим",
201
- "proc.manage_hint": "Проверить вывод: process_log id={id}. Остановить: process_kill id={id}. Список всех: process_list.",
201
+ "proc.manage_hint": "Проверить вывод: process_log id={id}. Дождаться завершения: process_wait id={id}. Остановить: process_kill id={id}. Список всех: process_list.",
202
202
  "proc.output_preview": "Первые строки вывода:\n{lines}",
203
203
  "proc.none": "Фоновых процессов нет.",
204
204
  "proc.not_found": "Процесс не найден: {id}",
@@ -213,9 +213,13 @@
213
213
  "proc.status_running": "работает",
214
214
  "proc.status_exited": "завершён",
215
215
  "proc.status_killed": "остановлен",
216
+ "proc.wait_exited": "Процесс {id} {status} за {ms} мс с кодом {code}.",
217
+ "proc.wait_running": "Процесс {id} всё ещё работает (ждали {ms} мс).",
218
+ "proc.wait_hint": "Подождать ещё: process_wait id={id}. Проверить вывод: process_log id={id}. Остановить: process_kill id={id}.",
216
219
  "tool.friendly.process_list": "Список фоновых процессов",
217
220
  "tool.friendly.process_log": "Вывод процесса",
218
221
  "tool.friendly.process_kill": "Остановка процесса",
222
+ "tool.friendly.process_wait": "Ожидание процесса",
219
223
  "plan.no_steps": "Не указаны шаги плана. Укажите конкретные шаги с файлами и командами.",
220
224
  "plan.created": "План создан: {title} ({steps} шагов)",
221
225
  "plan.coverage_warning": "План может не покрывать требуемые файлы из постановки: {missing}. Добавьте шаги, покрывающие их.",
@@ -413,6 +417,7 @@
413
417
  "cli.export_format": "Формат вывода: md|json|jsonl (по умолчанию md)",
414
418
  "cli.export_out": "Путь вывода ('-' для stdout)",
415
419
  "cli.export_no_redact": "Не скрывать секреты",
420
+ "cli.export_compact": "Пропустить тела результатов тулов (компактный читаемый экспорт)",
416
421
  "cli.first_run": "Первый запуск. Запускаем мастер настройки...",
417
422
  "cli.first_run_oneshot": "Конфигурация не найдена — запуск с настройками по умолчанию. Запустите `mma` без аргументов один раз для настройки.",
418
423
  "cli.web_setup_required": "Конфигурация не найдена — открывается мастер настройки в браузере.",
@@ -617,7 +622,7 @@
617
622
  "repl.help_aliases": "Алиасы: {names}",
618
623
  "repl.help_examples": "Примеры:",
619
624
  "repl.export": "Экспортировать сессию в файл",
620
- "repl.export_usage": "Использование: /export [<id|имя>] [--format md|json|jsonl] [--out <путь>] [--no-redact]",
625
+ "repl.export_usage": "Использование: /export [<id|имя>] [--format md|json|jsonl] [--out <путь>] [--no-redact] [--compact]",
621
626
  "config.header": "Управляемые настройки",
622
627
  "config.col_setting": "Настройка",
623
628
  "config.col_value": "Значение",
@@ -749,6 +754,7 @@
749
754
  "exec.tool_errors": "Инструмент {tool} упал {count} раз. Попробуйте другой инструмент.",
750
755
  "exec.tool_errors_recovery": "Инструмент {tool} упал {count} раз подряд. Попробуйте альтернативу: создайте файлы напрямую через write_file, используйте другую команду, или пропустите этот шаг через plan update step=N status=skipped с пометкой почему.",
751
756
  "exec.repetitive_tool": "Инструмент {tool} вызван {count} раз с одинаковыми аргументами и результатом. Попробуйте другой подход — создайте файлы напрямую, измените аргументы или проверьте статус процесса через process_log.",
757
+ "exec.repetitive_process_log_hint": "Вы опрашиваете фоновый процесс в цикле — вызовите process_wait один раз вместо опроса через process_log.",
752
758
  "exec.consecutive_failures_recovery": "{count} инструментов подряд упали. Создавайте файлы напрямую через write_file вместо команд терминала. Проверьте что зависимости установлены (npm install). Не запускайте сборку/тесты пока не созданы все файлы.",
753
759
  "exec.bash_flailing_recovery": "bash упал в {fails} из последних {window} попыток. Хватит использовать шелл-команды для файловых операций на этой машине — они не работают. Используй специализированные инструменты: read_file (в заголовке число строк), write_file, edit_file, file_info (размер файла), glob/grep для поиска",
754
760
  "exec.bash_flailing": "bash упал в {fails} из последних {window} попыток.",
@@ -791,6 +797,7 @@
791
797
  "hall.uncertainty": "Маркеры неопределённости: {markers}",
792
798
  "hall.unknown_paths": "Упомянутые файлы не найдены: {paths}",
793
799
  "hall.contradiction_llm": "Противоречит ранее принятому решению{reason}",
800
+ "hall.claim_contradiction": "Ответ заявляет об успехе, но свежий результат тула указывает на проблему ({tool}: {snippet}). Либо проверь и исправь, затем перезапусти проверку — либо смягчи формулировку под факты (например, застейджено, появится в ближайшее время).",
794
801
  "hall.uncertainty_prefix": "\n\n[⚠️ Неопределённость] ",
795
802
  "browser.repeated_action": "Вы повторили то же действие ({action}) {threshold} раз. Попробуйте другой подход: используйте \"snapshot\" для перечитки страницы, другие номера элементов или другой URL.",
796
803
  "browser.unknown_action": "Неизвестное действие: {action}",
@@ -1194,8 +1201,8 @@
1194
1201
  "web.selectors.current": "текущее",
1195
1202
  "web.selectors.context_prompt": "контекстное окно (токены)",
1196
1203
  "web.action.thinking": "думаю",
1197
- "web.action.hide": "▾ скрыть",
1198
- "web.action.show": "▸ показать",
1204
+ "web.action.hide": "скрыть",
1205
+ "web.action.show": "показать",
1199
1206
  "web.action.actions_one": "{count} действие",
1200
1207
  "web.action.actions_few": "{count} действия",
1201
1208
  "web.action.actions_many": "{count} действий",
package/dist/main.js CHANGED
@@ -2671,7 +2671,7 @@ Iterations: {iterations}`,
2671
2671
  "proc.started": `Started background process {id} (PID {pid}).
2672
2672
  Command: {command}`,
2673
2673
  "proc.promoted_hint": "Command still running after {ms} ms — moved to the background",
2674
- "proc.manage_hint": "Check output: process_log id={id}. Stop it: process_kill id={id}. List all: process_list.",
2674
+ "proc.manage_hint": "Check output: process_log id={id}. Wait for completion: process_wait id={id}. Stop it: process_kill id={id}. List all: process_list.",
2675
2675
  "proc.output_preview": `First output:
2676
2676
  {lines}`,
2677
2677
  "proc.none": "No background processes running.",
@@ -2687,9 +2687,13 @@ Command: {command}`,
2687
2687
  "proc.status_running": "running",
2688
2688
  "proc.status_exited": "exited",
2689
2689
  "proc.status_killed": "killed",
2690
+ "proc.wait_exited": "Process {id} {status} after {ms} ms with exit code {code}.",
2691
+ "proc.wait_running": "Process {id} still running after {ms} ms.",
2692
+ "proc.wait_hint": "Wait again: process_wait id={id}. Check output: process_log id={id}. Stop it: process_kill id={id}.",
2690
2693
  "tool.friendly.process_list": "Listing background processes",
2691
2694
  "tool.friendly.process_log": "Process output",
2692
2695
  "tool.friendly.process_kill": "Stopping process",
2696
+ "tool.friendly.process_wait": "Waiting for process",
2693
2697
  "plan.no_steps": "No plan steps specified. Provide concrete steps with files and commands.",
2694
2698
  "plan.created": "Plan created: {title} ({steps} steps)",
2695
2699
  "plan.coverage_warning": "Plan may be missing required files from the task: {missing}. Add steps covering them.",
@@ -2893,6 +2897,7 @@ Fix the error and re-edit the file (a clean write clears the failure), or mark t
2893
2897
  "cli.export_format": "Output format: md|json|jsonl (default md)",
2894
2898
  "cli.export_out": "Output path ('-' for stdout)",
2895
2899
  "cli.export_no_redact": "Do not redact secrets",
2900
+ "cli.export_compact": "Omit tool result bodies (compact readable export)",
2896
2901
  "cli.first_run": "First run detected. Running setup wizard...",
2897
2902
  "cli.first_run_oneshot": "No configuration found — running with defaults. Run `mma` (no arguments) once to set up the wizard.",
2898
2903
  "cli.web_setup_required": "No configuration found — opening the browser setup wizard.",
@@ -3105,7 +3110,7 @@ Excluded blocks: {count}`,
3105
3110
  "repl.help_aliases": "Aliases: {names}",
3106
3111
  "repl.help_examples": "Examples:",
3107
3112
  "repl.export": "Export a session to a file",
3108
- "repl.export_usage": "Usage: /export [<id|name>] [--format md|json|jsonl] [--out <path>] [--no-redact]",
3113
+ "repl.export_usage": "Usage: /export [<id|name>] [--format md|json|jsonl] [--out <path>] [--no-redact] [--compact]",
3109
3114
  "config.header": "Managed settings",
3110
3115
  "config.col_setting": "Setting",
3111
3116
  "config.col_value": "Value",
@@ -3248,6 +3253,7 @@ Use this knowledge to answer the user's question.`,
3248
3253
  "exec.tool_errors": "Tool {tool} failed {count} times. Consider using a different tool.",
3249
3254
  "exec.tool_errors_recovery": "Tool {tool} has failed {count} times in a row. Try an alternative: create files directly via write_file, use a different command, or if nothing works — skip this step via plan update step=N status=skipped with a note explaining why.",
3250
3255
  "exec.repetitive_tool": "Called {tool} {count} times with identical arguments and result. Try a different approach — create files directly, change arguments, or check process status via process_log.",
3256
+ "exec.repetitive_process_log_hint": "You are polling a background process — call process_wait once instead of polling process_log.",
3251
3257
  "exec.consecutive_failures_recovery": "{count} consecutive tool failures. Create files directly via write_file instead of terminal commands. Check that dependencies are installed (npm install). Do not run build/tests until all files are created.",
3252
3258
  "exec.bash_flailing_recovery": "bash failed in {fails} of the last {window} attempts. Stop using shell commands for file operations on this machine — they do not work. Use the dedicated tools instead: read_file (the header shows line counts), write_file, edit_file, file_info (file size), glob/grep for search",
3253
3259
  "exec.bash_flailing": "bash failed in {fails} of the last {window} attempts.",
@@ -3296,6 +3302,7 @@ Apply a matching solution from these results. If none is relevant — do NOT rep
3296
3302
  "hall.uncertainty": "Uncertainty markers: {markers}",
3297
3303
  "hall.unknown_paths": "Mentioned file paths not found in known files: {paths}",
3298
3304
  "hall.contradiction_llm": "Contradicts an earlier decision{reason}",
3305
+ "hall.claim_contradiction": "The answer claims success, but a recent tool result indicates a problem ({tool}: {snippet}). Either verify and fix, then re-run the check — or soften the claim to match the evidence (e.g. staged, will appear shortly).",
3299
3306
  "hall.uncertainty_prefix": `
3300
3307
 
3301
3308
  [⚠️ Uncertainty] `,
@@ -3737,8 +3744,8 @@ Updated: {updatedAt}`,
3737
3744
  "web.selectors.current": "current",
3738
3745
  "web.selectors.context_prompt": "context window (tokens)",
3739
3746
  "web.action.thinking": "thinking",
3740
- "web.action.hide": "▾ hide",
3741
- "web.action.show": "▸ show",
3747
+ "web.action.hide": "hide",
3748
+ "web.action.show": "show",
3742
3749
  "web.action.actions_one": "{count} action",
3743
3750
  "web.action.actions_few": "{count} actions",
3744
3751
  "web.action.actions_many": "{count} actions",
@@ -4094,7 +4101,7 @@ var init_ru = __esm(() => {
4094
4101
  "proc.started": `Фоновый процесс запущен: {id} (PID {pid}).
4095
4102
  Команда: {command}`,
4096
4103
  "proc.promoted_hint": "Команда всё ещё выполняется через {ms} мс — переведена в фоновый режим",
4097
- "proc.manage_hint": "Проверить вывод: process_log id={id}. Остановить: process_kill id={id}. Список всех: process_list.",
4104
+ "proc.manage_hint": "Проверить вывод: process_log id={id}. Дождаться завершения: process_wait id={id}. Остановить: process_kill id={id}. Список всех: process_list.",
4098
4105
  "proc.output_preview": `Первые строки вывода:
4099
4106
  {lines}`,
4100
4107
  "proc.none": "Фоновых процессов нет.",
@@ -4110,9 +4117,13 @@ var init_ru = __esm(() => {
4110
4117
  "proc.status_running": "работает",
4111
4118
  "proc.status_exited": "завершён",
4112
4119
  "proc.status_killed": "остановлен",
4120
+ "proc.wait_exited": "Процесс {id} {status} за {ms} мс с кодом {code}.",
4121
+ "proc.wait_running": "Процесс {id} всё ещё работает (ждали {ms} мс).",
4122
+ "proc.wait_hint": "Подождать ещё: process_wait id={id}. Проверить вывод: process_log id={id}. Остановить: process_kill id={id}.",
4113
4123
  "tool.friendly.process_list": "Список фоновых процессов",
4114
4124
  "tool.friendly.process_log": "Вывод процесса",
4115
4125
  "tool.friendly.process_kill": "Остановка процесса",
4126
+ "tool.friendly.process_wait": "Ожидание процесса",
4116
4127
  "plan.no_steps": "Не указаны шаги плана. Укажите конкретные шаги с файлами и командами.",
4117
4128
  "plan.created": "План создан: {title} ({steps} шагов)",
4118
4129
  "plan.coverage_warning": "План может не покрывать требуемые файлы из постановки: {missing}. Добавьте шаги, покрывающие их.",
@@ -4312,6 +4323,7 @@ var init_ru = __esm(() => {
4312
4323
  "cli.export_format": "Формат вывода: md|json|jsonl (по умолчанию md)",
4313
4324
  "cli.export_out": "Путь вывода ('-' для stdout)",
4314
4325
  "cli.export_no_redact": "Не скрывать секреты",
4326
+ "cli.export_compact": "Пропустить тела результатов тулов (компактный читаемый экспорт)",
4315
4327
  "cli.first_run": "Первый запуск. Запускаем мастер настройки...",
4316
4328
  "cli.first_run_oneshot": "Конфигурация не найдена — запуск с настройками по умолчанию. Запустите `mma` без аргументов один раз для настройки.",
4317
4329
  "cli.web_setup_required": "Конфигурация не найдена — открывается мастер настройки в браузере.",
@@ -4524,7 +4536,7 @@ var init_ru = __esm(() => {
4524
4536
  "repl.help_aliases": "Алиасы: {names}",
4525
4537
  "repl.help_examples": "Примеры:",
4526
4538
  "repl.export": "Экспортировать сессию в файл",
4527
- "repl.export_usage": "Использование: /export [<id|имя>] [--format md|json|jsonl] [--out <путь>] [--no-redact]",
4539
+ "repl.export_usage": "Использование: /export [<id|имя>] [--format md|json|jsonl] [--out <путь>] [--no-redact] [--compact]",
4528
4540
  "config.header": "Управляемые настройки",
4529
4541
  "config.col_setting": "Настройка",
4530
4542
  "config.col_value": "Значение",
@@ -4671,6 +4683,7 @@ var init_ru = __esm(() => {
4671
4683
  "exec.tool_errors": "Инструмент {tool} упал {count} раз. Попробуйте другой инструмент.",
4672
4684
  "exec.tool_errors_recovery": "Инструмент {tool} упал {count} раз подряд. Попробуйте альтернативу: создайте файлы напрямую через write_file, используйте другую команду, или пропустите этот шаг через plan update step=N status=skipped с пометкой почему.",
4673
4685
  "exec.repetitive_tool": "Инструмент {tool} вызван {count} раз с одинаковыми аргументами и результатом. Попробуйте другой подход — создайте файлы напрямую, измените аргументы или проверьте статус процесса через process_log.",
4686
+ "exec.repetitive_process_log_hint": "Вы опрашиваете фоновый процесс в цикле — вызовите process_wait один раз вместо опроса через process_log.",
4674
4687
  "exec.consecutive_failures_recovery": "{count} инструментов подряд упали. Создавайте файлы напрямую через write_file вместо команд терминала. Проверьте что зависимости установлены (npm install). Не запускайте сборку/тесты пока не созданы все файлы.",
4675
4688
  "exec.bash_flailing_recovery": "bash упал в {fails} из последних {window} попыток. Хватит использовать шелл-команды для файловых операций на этой машине — они не работают. Используй специализированные инструменты: read_file (в заголовке число строк), write_file, edit_file, file_info (размер файла), glob/grep для поиска",
4676
4689
  "exec.bash_flailing": "bash упал в {fails} из последних {window} попыток.",
@@ -4719,6 +4732,7 @@ var init_ru = __esm(() => {
4719
4732
  "hall.uncertainty": "Маркеры неопределённости: {markers}",
4720
4733
  "hall.unknown_paths": "Упомянутые файлы не найдены: {paths}",
4721
4734
  "hall.contradiction_llm": "Противоречит ранее принятому решению{reason}",
4735
+ "hall.claim_contradiction": "Ответ заявляет об успехе, но свежий результат тула указывает на проблему ({tool}: {snippet}). Либо проверь и исправь, затем перезапусти проверку — либо смягчи формулировку под факты (например, застейджено, появится в ближайшее время).",
4722
4736
  "hall.uncertainty_prefix": `
4723
4737
 
4724
4738
  [⚠️ Неопределённость] `,
@@ -5139,8 +5153,8 @@ var init_ru = __esm(() => {
5139
5153
  "web.selectors.current": "текущее",
5140
5154
  "web.selectors.context_prompt": "контекстное окно (токены)",
5141
5155
  "web.action.thinking": "думаю",
5142
- "web.action.hide": "▾ скрыть",
5143
- "web.action.show": "▸ показать",
5156
+ "web.action.hide": "скрыть",
5157
+ "web.action.show": "показать",
5144
5158
  "web.action.actions_one": "{count} действие",
5145
5159
  "web.action.actions_few": "{count} действия",
5146
5160
  "web.action.actions_many": "{count} действий",
@@ -9154,10 +9168,21 @@ var init_runner = __esm(() => {
9154
9168
  });
9155
9169
 
9156
9170
  // src/tools/executor.ts
9171
+ function stableArgsKey(value) {
9172
+ if (Array.isArray(value))
9173
+ return value.map(stableArgsKey);
9174
+ if (value && typeof value === "object") {
9175
+ const entries = Object.keys(value).sort().map((k) => [k, stableArgsKey(value[k])]);
9176
+ return Object.fromEntries(entries);
9177
+ }
9178
+ return value;
9179
+ }
9180
+
9157
9181
  class ToolExecutor {
9158
9182
  registry;
9159
9183
  ctx;
9160
9184
  pluginManager;
9185
+ memo = new Map;
9161
9186
  constructor(registry, ctx, pluginManager) {
9162
9187
  this.registry = registry;
9163
9188
  this.ctx = ctx;
@@ -9180,6 +9205,31 @@ class ToolExecutor {
9180
9205
  hasTool(name) {
9181
9206
  return this.registry.has(name);
9182
9207
  }
9208
+ memoKey(name, args) {
9209
+ return `${this.ctx.sessionId ?? ""}::${name}::${JSON.stringify(stableArgsKey(args))}`;
9210
+ }
9211
+ memoGet(name, args) {
9212
+ const hit = this.memo.get(this.memoKey(name, args));
9213
+ if (!hit)
9214
+ return;
9215
+ if (Date.now() - hit.at > MEMO_TTL_MS) {
9216
+ this.memo.delete(this.memoKey(name, args));
9217
+ return;
9218
+ }
9219
+ return { ...hit.result };
9220
+ }
9221
+ memoSet(name, args, result) {
9222
+ const key = this.memoKey(name, args);
9223
+ if (!this.memo.has(key) && this.memo.size >= MEMO_MAX_ENTRIES) {
9224
+ const oldest = this.memo.keys().next();
9225
+ if (!oldest.done)
9226
+ this.memo.delete(oldest.value);
9227
+ }
9228
+ this.memo.set(key, { result: { ...result }, at: Date.now() });
9229
+ }
9230
+ memoInvalidate() {
9231
+ this.memo.clear();
9232
+ }
9183
9233
  allowedNames() {
9184
9234
  const allow = this.ctx.toolAllowlist;
9185
9235
  return allow ? new Set(allow) : null;
@@ -9221,10 +9271,11 @@ class ToolExecutor {
9221
9271
  }
9222
9272
  }
9223
9273
  let result;
9274
+ const memoHit = tool.cacheable ? this.memoGet(call.name, call.arguments) : undefined;
9224
9275
  try {
9225
9276
  this.ctx.activeCallId = call.id;
9226
9277
  if (tool.interactive) {
9227
- result = await tool.handler(this.ctx, call.arguments);
9278
+ result = memoHit ?? await tool.handler(this.ctx, call.arguments);
9228
9279
  } else {
9229
9280
  const timeoutMs = tool.timeoutMs ?? TOOL_EXECUTION_TIMEOUT_MS;
9230
9281
  const failFast = (message) => {
@@ -9248,7 +9299,7 @@ class ToolExecutor {
9248
9299
  }, timeoutMs);
9249
9300
  });
9250
9301
  const racePromises = [
9251
- tool.handler(this.ctx, call.arguments),
9302
+ memoHit ? Promise.resolve(memoHit) : tool.handler(this.ctx, call.arguments),
9252
9303
  timeoutPromise
9253
9304
  ];
9254
9305
  if (signal) {
@@ -9284,6 +9335,13 @@ class ToolExecutor {
9284
9335
  toolCallId: call.id
9285
9336
  };
9286
9337
  }
9338
+ if (!memoHit) {
9339
+ if (tool.cacheable && result.success) {
9340
+ this.memoSet(call.name, call.arguments, result);
9341
+ } else if (!tool.cacheable && !tool.manualMemo) {
9342
+ this.memoInvalidate();
9343
+ }
9344
+ }
9287
9345
  if (signal?.aborted) {
9288
9346
  this.ctx.logger.debug(`Tool ${call.name}: abort, skipping onAfterTool plugins`);
9289
9347
  } else {
@@ -9337,7 +9395,7 @@ class ToolExecutor {
9337
9395
  this.ctx.llmProvider = provider;
9338
9396
  }
9339
9397
  }
9340
- var TOOL_EXECUTION_TIMEOUT_MS = 60000;
9398
+ var TOOL_EXECUTION_TIMEOUT_MS = 60000, MEMO_TTL_MS = 120000, MEMO_MAX_ENTRIES = 100;
9341
9399
  var init_executor = __esm(() => {
9342
9400
  init_runner();
9343
9401
  init_i18n();
@@ -10030,6 +10088,7 @@ var init_read_file = __esm(() => {
10030
10088
  description: `Read a file from the filesystem. Reads up to ${DEFAULT_LIMIT} lines at a time by default; use offset to page through large files.`,
10031
10089
  tags: ["file", "code"],
10032
10090
  boundedOutput: true,
10091
+ cacheable: true,
10033
10092
  parameters: {
10034
10093
  type: "object",
10035
10094
  properties: {
@@ -10957,6 +11016,7 @@ var init_glob_tool = __esm(() => {
10957
11016
  icon: "\uD83D\uDD0E",
10958
11017
  description: `Search for files matching a glob pattern. Shows up to ${MAX_PREVIEW_LINES} results by default. Uses standard glob syntax (e.g., **/*.ts, src/**/*.test.ts).`,
10959
11018
  tags: ["file", "code", "research"],
11019
+ cacheable: true,
10960
11020
  parameters: {
10961
11021
  type: "object",
10962
11022
  properties: {
@@ -11041,6 +11101,7 @@ var init_grep_tool = __esm(() => {
11041
11101
  icon: "\uD83D\uDD0D",
11042
11102
  description: `Search file contents using a regular expression. Shows up to ${MAX_PREVIEW_LINES} matching lines by default. Uses ripgrep (rg) if available, otherwise falls back to grep -r.`,
11043
11103
  tags: ["file", "code", "research"],
11104
+ cacheable: true,
11044
11105
  parameters: {
11045
11106
  type: "object",
11046
11107
  properties: {
@@ -11124,6 +11185,7 @@ var init_list_dir = __esm(() => {
11124
11185
  description: `List files and directories in a given path. Shows up to ${MAX_PREVIEW_LINES} entries by default.`,
11125
11186
  tags: ["file"],
11126
11187
  boundedOutput: true,
11188
+ cacheable: true,
11127
11189
  parameters: {
11128
11190
  type: "object",
11129
11191
  properties: {
@@ -11388,6 +11450,7 @@ var init_file_info = __esm(() => {
11388
11450
  description: "Get metadata about a file or directory (size, creation date, modification date).",
11389
11451
  tags: ["file"],
11390
11452
  boundedOutput: true,
11453
+ cacheable: true,
11391
11454
  parameters: {
11392
11455
  type: "object",
11393
11456
  properties: {
@@ -12048,6 +12111,26 @@ function emptyCliRunHint(command, output, code) {
12048
12111
  return null;
12049
12112
  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.";
12050
12113
  }
12114
+ function isMemoizableCommand(command) {
12115
+ const cmd = command.trim().toLowerCase();
12116
+ if (!cmd || /[|<>;&]/.test(cmd))
12117
+ return false;
12118
+ return READONLY_COMMAND_PATTERNS.some((re) => re.test(cmd));
12119
+ }
12120
+ function memoRead(ctx, args) {
12121
+ const get = ctx.toolExecutor?.memoGet;
12122
+ return typeof get === "function" ? get.call(ctx.toolExecutor, "bash", args) : undefined;
12123
+ }
12124
+ function memoWrite(ctx, args, result) {
12125
+ const set = ctx.toolExecutor?.memoSet;
12126
+ if (typeof set === "function")
12127
+ set.call(ctx.toolExecutor, "bash", args, result);
12128
+ }
12129
+ function memoClear(ctx) {
12130
+ const clear = ctx.toolExecutor?.memoInvalidate;
12131
+ if (typeof clear === "function")
12132
+ clear.call(ctx.toolExecutor);
12133
+ }
12051
12134
  function resolveBashSecurityConfig(ctx) {
12052
12135
  const appConfig = ctx.config || {};
12053
12136
  const fullSecurityConfig = ctx.sessionContext ? getSessionSecurityConfig(appConfig, ctx.sessionContext) : appConfig.security || DEFAULT_SECURITY_CONFIG;
@@ -12137,7 +12220,7 @@ Hint: ${t("exec.interactive_hint")}`;
12137
12220
  }
12138
12221
  return output;
12139
12222
  }
12140
- var BASH_GRACE_MS = 5000, SPAWN_SETTLE_MS = 100, BG_OUTPUT_PREVIEW_LINES = 15, bashGraceMs, FAILING_FIRST_WORDS, HARD_BLOCK_THRESHOLD = 3, lastFailureSessionId, UNIX_TO_WIN_HINTS, UNIX_TO_WIN_TRANSLATE, NEVER_TOOL_CALLS, CLI_FILE_RUN_RE, NPM_EXEC_RE, INTERACTIVE_PROMPT_RE, HIDDEN_PROCESS_TOOLS, bashTool;
12223
+ var BASH_GRACE_MS = 5000, SPAWN_SETTLE_MS = 100, BG_OUTPUT_PREVIEW_LINES = 15, bashGraceMs, FAILING_FIRST_WORDS, HARD_BLOCK_THRESHOLD = 3, lastFailureSessionId, UNIX_TO_WIN_HINTS, UNIX_TO_WIN_TRANSLATE, NEVER_TOOL_CALLS, CLI_FILE_RUN_RE, NPM_EXEC_RE, INTERACTIVE_PROMPT_RE, HIDDEN_PROCESS_TOOLS, READONLY_COMMAND_PREFIXES, READONLY_COMMAND_PATTERNS, bashTool;
12141
12224
  var init_bash = __esm(() => {
12142
12225
  init_command_validator();
12143
12226
  init_audit_log();
@@ -12216,11 +12299,41 @@ var init_bash = __esm(() => {
12216
12299
  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/;
12217
12300
  NPM_EXEC_RE = /could not determine executable to run/i;
12218
12301
  INTERACTIVE_PROMPT_RE = /operation (?:cancelled|canceled|aborted)|stdin is not a tty|not a tty|cannot prompt without a tty/i;
12219
- HIDDEN_PROCESS_TOOLS = ["process_list", "process_log", "process_kill"];
12302
+ HIDDEN_PROCESS_TOOLS = ["process_list", "process_log", "process_kill", "process_wait"];
12303
+ READONLY_COMMAND_PREFIXES = [
12304
+ "git status",
12305
+ "git log",
12306
+ "git diff",
12307
+ "git branch",
12308
+ "git tag",
12309
+ "git rev-parse",
12310
+ "git ls-remote",
12311
+ "git remote",
12312
+ "git show",
12313
+ "npm view",
12314
+ "npm list",
12315
+ "npm config get",
12316
+ "npm whoami",
12317
+ "npm ping",
12318
+ "npm --version",
12319
+ "node --version",
12320
+ "python --version",
12321
+ "python3 --version",
12322
+ "bun --version",
12323
+ "git --version",
12324
+ "dir",
12325
+ "ls",
12326
+ "echo",
12327
+ "type",
12328
+ "where",
12329
+ "which",
12330
+ "cat"
12331
+ ];
12332
+ READONLY_COMMAND_PATTERNS = READONLY_COMMAND_PREFIXES.map((p) => new RegExp(`^${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`));
12220
12333
  bashTool = {
12221
12334
  name: "bash",
12222
12335
  icon: "\uD83D\uDCBB",
12223
- 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).
12336
+ 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).
12224
12337
 
12225
12338
  Windows 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 &).`,
12226
12339
  tags: ["shell", "code"],
@@ -12239,6 +12352,7 @@ Windows notes: the shell is cmd.exe — PowerShell cmdlets (Write-Host, Get-Cont
12239
12352
  },
12240
12353
  required: ["command"]
12241
12354
  },
12355
+ manualMemo: true,
12242
12356
  handler: async (ctx, args) => {
12243
12357
  const originalCommand = String(args.command);
12244
12358
  const toolCall = detectToolCallInBash(originalCommand);
@@ -12281,6 +12395,15 @@ Hint: run one plain command per tool call. For output redirection write the file
12281
12395
  if (securityConfig?.logCommands) {
12282
12396
  logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, `Working directory: ${workdir}`);
12283
12397
  }
12398
+ const memoKey = { command, workdir };
12399
+ const memoizable = args.background !== true && isMemoizableCommand(command);
12400
+ if (memoizable) {
12401
+ const hit = memoRead(ctx, memoKey);
12402
+ if (hit)
12403
+ return { ...hit };
12404
+ } else {
12405
+ memoClear(ctx);
12406
+ }
12284
12407
  const entry = processRegistry.start(command, workdir, ctx.sessionId);
12285
12408
  if (ctx.activeCallId) {
12286
12409
  registerKillable(ctx.activeCallId, () => processRegistry.kill(entry.id));
@@ -12311,6 +12434,9 @@ Hint: check the "workdir" path exists and the command is valid for this OS.`
12311
12434
  workdir,
12312
12435
  securityConfig
12313
12436
  });
12437
+ if (memoizable && code === 0) {
12438
+ memoWrite(ctx, memoKey, { success: true, output: output2 });
12439
+ }
12314
12440
  return { success: code === 0, output: output2 };
12315
12441
  }
12316
12442
  const explicit = args.background === true;
@@ -12386,16 +12512,41 @@ ${t("proc.hint", {
12386
12512
  };
12387
12513
  });
12388
12514
 
12515
+ // src/tools/process-ids.ts
12516
+ function missingProcessIdHint() {
12517
+ const ids = processRegistry.list().map((p) => p.id);
12518
+ const listPart = ids.length ? `
12519
+ ${t("proc.list_header")}: ${ids.join(", ")}` : `
12520
+ ${t("proc.missing_id_list")}`;
12521
+ return `${t("proc.missing_id")}${listPart}`;
12522
+ }
12523
+ function lookupProcess(args) {
12524
+ if (args.id === undefined || String(args.id).trim() === "") {
12525
+ return { ok: false, output: missingProcessIdHint() };
12526
+ }
12527
+ const id = String(args.id);
12528
+ const entry = processRegistry.get(id);
12529
+ if (!entry) {
12530
+ return { ok: false, output: t("proc.not_found", { id }) };
12531
+ }
12532
+ return { ok: true, id, entry };
12533
+ }
12534
+ var init_process_ids = __esm(() => {
12535
+ init_processes();
12536
+ init_i18n();
12537
+ });
12538
+
12389
12539
  // src/tools/process-log.ts
12390
12540
  var DEFAULT_TAIL, MAX_TAIL = 300, processLogTool;
12391
12541
  var init_process_log = __esm(() => {
12392
12542
  init_processes();
12393
12543
  init_i18n();
12544
+ init_process_ids();
12394
12545
  DEFAULT_TAIL = MAX_PREVIEW_LINES;
12395
12546
  processLogTool = {
12396
12547
  name: "process_log",
12397
12548
  icon: "\uD83D\uDCDC",
12398
- description: `Show the buffered output of a background process started via bash. Shows the last ${DEFAULT_TAIL} lines by default. Use after starting a dev server to verify it came up without errors, and while it runs to check its state.`,
12549
+ description: `Show the buffered output of a background process started via bash. Shows the last ${DEFAULT_TAIL} lines by default. Use after starting a dev server to verify it came up without errors, and while it runs to check its state. For a finite task that should complete, prefer process_wait over polling this tool.`,
12399
12550
  tags: ["shell"],
12400
12551
  boundedOutput: true,
12401
12552
  parameters: {
@@ -12410,19 +12561,10 @@ var init_process_log = __esm(() => {
12410
12561
  required: ["id"]
12411
12562
  },
12412
12563
  handler: async (_ctx, args) => {
12413
- if (args.id === undefined || String(args.id).trim() === "") {
12414
- const known = processRegistry.list();
12415
- const ids = known.map((p) => p.id);
12416
- const listPart = ids.length ? `
12417
- ${t("proc.list_header")}: ${ids.join(", ")}` : `
12418
- ${t("proc.missing_id_list")}`;
12419
- return { success: false, output: `${t("proc.missing_id")}${listPart}` };
12420
- }
12421
- const id = String(args.id);
12422
- const entry = processRegistry.get(id);
12423
- if (!entry) {
12424
- return { success: false, output: t("proc.not_found", { id }) };
12425
- }
12564
+ const found = lookupProcess(args);
12565
+ if (!found.ok)
12566
+ return { success: false, output: found.output };
12567
+ const { id, entry } = found;
12426
12568
  const status = entry.status === "running" ? t("proc.status_running") : entry.status === "exited" ? t("proc.status_exited") : t("proc.status_killed");
12427
12569
  const tail = Math.min(MAX_TAIL, Math.max(1, typeof args.tail === "number" ? Math.floor(args.tail) : DEFAULT_TAIL));
12428
12570
  const log = processRegistry.getLog(id, tail);
@@ -12446,6 +12588,7 @@ var processKillTool;
12446
12588
  var init_process_kill = __esm(() => {
12447
12589
  init_processes();
12448
12590
  init_i18n();
12591
+ init_process_ids();
12449
12592
  processKillTool = {
12450
12593
  name: "process_kill",
12451
12594
  icon: "⛔",
@@ -12459,26 +12602,79 @@ var init_process_kill = __esm(() => {
12459
12602
  required: ["id"]
12460
12603
  },
12461
12604
  handler: async (_ctx, args) => {
12462
- if (args.id === undefined || String(args.id).trim() === "") {
12463
- const known = processRegistry.list();
12464
- const ids = known.map((p) => p.id);
12465
- const listPart = ids.length ? `
12466
- ${t("proc.list_header")}: ${ids.join(", ")}` : `
12467
- ${t("proc.missing_id_list")}`;
12468
- return { success: false, output: `${t("proc.missing_id")}${listPart}` };
12605
+ const found = lookupProcess(args);
12606
+ if (!found.ok)
12607
+ return { success: false, output: found.output };
12608
+ const { id, entry } = found;
12609
+ const killed = processRegistry.kill(id);
12610
+ if (!killed) {
12611
+ return { success: false, output: t("proc.kill_failed", { id }) };
12469
12612
  }
12470
- const id = String(args.id);
12613
+ return {
12614
+ success: true,
12615
+ output: t("proc.killed", { id, pid: entry.pid })
12616
+ };
12617
+ }
12618
+ };
12619
+ });
12620
+
12621
+ // src/tools/process-wait.ts
12622
+ var DEFAULT_WAIT_MS = 30000, MAX_WAIT_MS = 110000, DEFAULT_TAIL2 = 20, MAX_TAIL2 = 100, processWaitTool;
12623
+ var init_process_wait = __esm(() => {
12624
+ init_processes();
12625
+ init_i18n();
12626
+ init_process_ids();
12627
+ processWaitTool = {
12628
+ name: "process_wait",
12629
+ icon: "⏳",
12630
+ description: "Wait for a background process started via bash to finish, instead of polling process_log or sleeping (ping/timeout/sleep commands). Blocks up to timeoutMs, then reports whether the process exited (with its exit code and recent output) or is still running. For dev servers/watchers that never exit, use process_log to inspect and process_kill to stop them.",
12631
+ tags: ["shell"],
12632
+ boundedOutput: true,
12633
+ timeoutMs: 120000,
12634
+ parameters: {
12635
+ type: "object",
12636
+ properties: {
12637
+ id: { type: "string", description: "Process id from bash output or process_list" },
12638
+ timeoutMs: {
12639
+ type: "number",
12640
+ description: `How long to wait in milliseconds (default: ${DEFAULT_WAIT_MS}, max ${MAX_WAIT_MS})`
12641
+ },
12642
+ tail: {
12643
+ type: "number",
12644
+ description: `Number of trailing output lines to include (default: ${DEFAULT_TAIL2}, max ${MAX_TAIL2})`
12645
+ }
12646
+ },
12647
+ required: ["id"]
12648
+ },
12649
+ handler: async (_ctx, args) => {
12650
+ const found = lookupProcess(args);
12651
+ if (!found.ok)
12652
+ return { success: false, output: found.output };
12653
+ const { id } = found;
12654
+ const waitMs = Math.min(MAX_WAIT_MS, Math.max(1000, typeof args.timeoutMs === "number" ? Math.floor(args.timeoutMs) : DEFAULT_WAIT_MS));
12655
+ const started = Date.now();
12656
+ await processRegistry.waitForExit(id, waitMs);
12657
+ const elapsed = Date.now() - started;
12471
12658
  const entry = processRegistry.get(id);
12472
12659
  if (!entry) {
12473
12660
  return { success: false, output: t("proc.not_found", { id }) };
12474
12661
  }
12475
- const killed = processRegistry.kill(id);
12476
- if (!killed) {
12477
- return { success: false, output: t("proc.kill_failed", { id }) };
12662
+ const tail = Math.min(MAX_TAIL2, Math.max(1, typeof args.tail === "number" ? Math.floor(args.tail) : DEFAULT_TAIL2));
12663
+ const log = processRegistry.getLog(id, tail);
12664
+ const logPart = log ? `
12665
+ ${log}` : `
12666
+ ${t("proc.log_empty")}`;
12667
+ if (entry.status !== "running") {
12668
+ const status = entry.status === "exited" ? t("proc.status_exited") : t("proc.status_killed");
12669
+ return {
12670
+ success: true,
12671
+ output: `${t("proc.wait_exited", { id, ms: elapsed, status, code: entry.exitCode ?? "?" })}${logPart}`
12672
+ };
12478
12673
  }
12479
12674
  return {
12480
12675
  success: true,
12481
- output: t("proc.killed", { id, pid: entry.pid })
12676
+ output: `${t("proc.wait_running", { id, ms: elapsed })}
12677
+ ${t("proc.wait_hint", { id })}${logPart}`
12482
12678
  };
12483
12679
  }
12484
12680
  };
@@ -14943,10 +15139,14 @@ class StuckDetector {
14943
15139
  if (!this.hasRepetitiveToolCalls())
14944
15140
  return "";
14945
15141
  const last = this.recentToolCalls[this.recentToolCalls.length - 1];
14946
- return t("exec.repetitive_tool", {
15142
+ const base = t("exec.repetitive_tool", {
14947
15143
  tool: last.name,
14948
15144
  count: this.repetitionThreshold
14949
15145
  });
15146
+ if (last.name === "process_log") {
15147
+ return `${base} ${t("exec.repetitive_process_log_hint")}`;
15148
+ }
15149
+ return base;
14950
15150
  }
14951
15151
  getWarningKey() {
14952
15152
  if (this.isStuck())
@@ -14986,6 +15186,9 @@ class StuckDetector {
14986
15186
  if (this.hasConsecutiveFailures()) {
14987
15187
  return t("exec.consecutive_failures", { count: String(this.consecutiveFailures) });
14988
15188
  }
15189
+ if (this.hasRepetitiveToolCalls()) {
15190
+ return this.getRepetitiveToolMessage();
15191
+ }
14989
15192
  if (this.hasReadOnlyLoop()) {
14990
15193
  return t("exec.read_only_loop", {
14991
15194
  count: String(this.readOnlyStreak)
@@ -17084,7 +17287,7 @@ function evaluateReasoningPolicy(input, state) {
17084
17287
  var DECAY_THRESHOLD = 5;
17085
17288
 
17086
17289
  // src/core/agent/constants.ts
17087
- var TOOL_RESULT_MAX_TOKENS_RATIO = 0.3, TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000, QUALITY_TRIGGER_THRESHOLD = 40, TOOL_ARGS_SUMMARY_MAX_CHARS = 240, TOOL_ARG_VALUE_MAX_CHARS = 48, FORCED_COMPACTION_COOLDOWN = 3, MAX_LLM_ERROR_RETRIES = 2, MAX_HALLUCINATION_RETRIES = 3, MAX_CONSECUTIVE_TOOL_FAILURES = 5, MIN_REPEATED_TOOL_FAILURES = 3, MAX_AUDIT_RETRIES = 3, MAX_EMPTY_RESPONSE_RETRIES = 2, MAX_REPEATED_TOOL_CALLS = 2, MAX_REPEATED_TOOL_CALLS_INTERACTIVE = 5, MUTATION_CYCLE_NUDGE_THRESHOLD = 5;
17290
+ var TOOL_RESULT_MAX_TOKENS_RATIO = 0.3, TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000, QUALITY_TRIGGER_THRESHOLD = 40, TOOL_ARGS_SUMMARY_MAX_CHARS = 240, TOOL_ARG_VALUE_MAX_CHARS = 48, FORCED_COMPACTION_COOLDOWN = 3, MAX_LLM_ERROR_RETRIES = 2, MAX_HALLUCINATION_RETRIES = 3, MAX_CLAIM_CHECK_RETRIES = 1, MAX_CONSECUTIVE_TOOL_FAILURES = 5, MIN_REPEATED_TOOL_FAILURES = 3, MAX_AUDIT_RETRIES = 3, MAX_EMPTY_RESPONSE_RETRIES = 2, MAX_REPEATED_TOOL_CALLS = 2, MAX_REPEATED_TOOL_CALLS_INTERACTIVE = 5, MUTATION_CYCLE_NUDGE_THRESHOLD = 5;
17088
17291
 
17089
17292
  // src/core/agent/artifact-progress.ts
17090
17293
  function isTestOrSpecPath(filePath) {
@@ -17135,6 +17338,7 @@ function createLoopState() {
17135
17338
  lastModelText: "",
17136
17339
  lastForcedCompactionIteration: -FORCED_COMPACTION_COOLDOWN,
17137
17340
  hallucinationRetries: 0,
17341
+ claimCheckRetries: 0,
17138
17342
  lastToolSignature: "",
17139
17343
  consecutiveToolFailures: 0,
17140
17344
  consecutiveToolSuccesses: 0,
@@ -17772,7 +17976,51 @@ var init_tool_batch = __esm(() => {
17772
17976
  init_utils();
17773
17977
  });
17774
17978
 
17979
+ // src/modules/hallucination/success-claim.ts
17980
+ function checkSuccessClaim(response, tools) {
17981
+ if (!response || !SUCCESS_CLAIM_RE.test(response))
17982
+ return null;
17983
+ if (HEDGE_RE.test(response))
17984
+ return null;
17985
+ const recent = tools.filter((t2) => t2.text?.trim()).slice(-EVIDENCE_WINDOW);
17986
+ for (let i = recent.length - 1;i >= 0; i--) {
17987
+ const entry = recent[i];
17988
+ const text = entry.text.slice(0, EVIDENCE_TEXT_CAP);
17989
+ if (entry.ok === false || FAILURE_SIGNAL_RE.test(text)) {
17990
+ const firstLine = text.split(`
17991
+ `).map((l) => l.trim()).find((l) => l.length > 0);
17992
+ if (!firstLine)
17993
+ continue;
17994
+ return {
17995
+ tool: entry.name || "tool",
17996
+ snippet: firstLine.length > SNIPPET_MAX_CHARS ? `${firstLine.slice(0, SNIPPET_MAX_CHARS - 1)}…` : firstLine
17997
+ };
17998
+ }
17999
+ }
18000
+ return null;
18001
+ }
18002
+ var WORD = "A-Za-zА-Яа-яЁё0-9_", wb = (body) => new RegExp(`(?<![${WORD}])(?:${body})(?![${WORD}])`, "i"), SUCCESS_CLAIM_RE, HEDGE_RE, FAILURE_SIGNAL_RE, EVIDENCE_WINDOW = 10, EVIDENCE_TEXT_CAP = 400, SNIPPET_MAX_CHARS = 160;
18003
+ var init_success_claim = __esm(() => {
18004
+ SUCCESS_CLAIM_RE = wb("published|опубликован[а-яё]*|задеплоен[а-яё]*|deployed|shipped|готово|выполнено|завершен[а-яё]*|успешно|success(?:ful(?:ly)?)?|done|completed?|finished|все сделано|всё сделано");
18005
+ HEDGE_RE = wb("may take|being processed|processing|staged|propagat[a-z]*|in progress|shortly|soon|will appear|несколько минут|обрабатывается|застейджен[а-яё]*|подожд[а-яё]*|проверь[а-яё]* позже|появится|пропагир[а-яё]*|кэш[а-яё]*|cach[a-z]*|обычно|typically");
18006
+ FAILURE_SIGNAL_RE = new RegExp(`${wb("404|409|40[03]|50\\d|failed|failure|not found|ERR_[A-Z_]+|E(?:404|409|ACCES|PERM|CONNREFUSED|TIMED?OUT)|conflict|denied|ошибка|ошибк[а-яё]*|не найден[а-яё]*|не удалось|отклонено").source}|✗|⛔`, "i");
18007
+ });
18008
+
17775
18009
  // src/core/agent/hallucination-gate.ts
18010
+ function lastToolEvidence(deps) {
18011
+ let history;
18012
+ try {
18013
+ history = deps.contextManager.getActiveHistory() ?? [];
18014
+ } catch {
18015
+ return [];
18016
+ }
18017
+ return history.filter((m) => m.role === "tool").slice(-10).map((m) => ({
18018
+ name: m.name,
18019
+ ok: m.success,
18020
+ text: typeof m.content === "string" ? m.content : JSON.stringify(m.content) ?? ""
18021
+ })).filter((m) => m.text.trim().length > 0);
18022
+ }
18023
+
17776
18024
  class HallucinationGate {
17777
18025
  detector;
17778
18026
  constructor(detector) {
@@ -17794,36 +18042,66 @@ class HallucinationGate {
17794
18042
  return { action: "warn" };
17795
18043
  }
17796
18044
  if (result.status === "retry") {
17797
- state.iterHallucinationRetry = true;
17798
- if (deps.exitOnComplete && textContent?.trim() && !deps.hasFinalAudit) {
17799
- logger2.debug("Exit-on-complete: stopping on first response");
17800
- state.lastText = textContent;
17801
- state.finalAnswerAccepted = true;
17802
- return { action: "exit" };
17803
- }
17804
- if (state.suppressRepetitionRetry && result.kind === "repetition") {
17805
- state.suppressRepetitionRetry = false;
17806
- logger2.warn(`Audit-triggered re-answer repetition — not counted as hallucination retry`);
17807
- this.displayWarnLine(result.reason, deps);
17808
- return { action: "accept" };
17809
- }
17810
- if (state.hallucinationRetries >= MAX_HALLUCINATION_RETRIES) {
17811
- logger2.warn(`Hallucination retries exhausted (${MAX_HALLUCINATION_RETRIES}), returning error`);
17812
- return { action: "block", reason: t("hall.max_retries_exhausted") };
17813
- }
17814
- state.hallucinationRetries++;
17815
- logger2.warn(`Hallucination retry (${state.hallucinationRetries}/${MAX_HALLUCINATION_RETRIES}): ${result.reason}`);
17816
- if (textContent) {
17817
- deps.contextManager.addMessage({ role: "assistant", content: textContent });
17818
- }
17819
- deps.contextManager.addMessage({
17820
- role: "user",
17821
- content: `<system-summary>[Retry context: ${result.reason}. Original task: "${deps.input}". You must either call a needed tool or provide a substantive response. Empty replies are not allowed.]</system-summary>`
17822
- });
17823
- return { action: "retry" };
18045
+ return this.applyRetry(deps, textContent, result);
17824
18046
  }
18047
+ const claimRetry = this.checkSuccessClaim(deps, textContent);
18048
+ if (claimRetry)
18049
+ return claimRetry;
17825
18050
  return { action: "accept" };
17826
18051
  }
18052
+ applyRetry(deps, textContent, result) {
18053
+ const { state, logger: logger2 } = deps;
18054
+ state.iterHallucinationRetry = true;
18055
+ if (deps.exitOnComplete && textContent?.trim() && !deps.hasFinalAudit) {
18056
+ logger2.debug("Exit-on-complete: stopping on first response");
18057
+ state.lastText = textContent;
18058
+ state.finalAnswerAccepted = true;
18059
+ return { action: "exit" };
18060
+ }
18061
+ if (state.suppressRepetitionRetry && result.kind === "repetition") {
18062
+ state.suppressRepetitionRetry = false;
18063
+ logger2.warn(`Audit-triggered re-answer repetition — not counted as hallucination retry`);
18064
+ this.displayWarnLine(result.reason, deps);
18065
+ return { action: "accept" };
18066
+ }
18067
+ if (state.hallucinationRetries >= MAX_HALLUCINATION_RETRIES) {
18068
+ logger2.warn(`Hallucination retries exhausted (${MAX_HALLUCINATION_RETRIES}), returning error`);
18069
+ return { action: "block", reason: t("hall.max_retries_exhausted") };
18070
+ }
18071
+ state.hallucinationRetries++;
18072
+ logger2.warn(`Hallucination retry (${state.hallucinationRetries}/${MAX_HALLUCINATION_RETRIES}): ${result.reason}`);
18073
+ if (textContent) {
18074
+ deps.contextManager.addMessage({ role: "assistant", content: textContent });
18075
+ }
18076
+ deps.contextManager.addMessage({
18077
+ role: "user",
18078
+ content: `<system-summary>[Retry context: ${result.reason}. Original task: "${deps.input}". You must either call a needed tool or provide a substantive response. Empty replies are not allowed.]</system-summary>`
18079
+ });
18080
+ return { action: "retry" };
18081
+ }
18082
+ checkSuccessClaim(deps, textContent) {
18083
+ const { state, logger: logger2 } = deps;
18084
+ const contradiction = checkSuccessClaim(textContent ?? "", lastToolEvidence(deps));
18085
+ if (!contradiction || state.claimCheckRetries >= MAX_CLAIM_CHECK_RETRIES) {
18086
+ return null;
18087
+ }
18088
+ state.claimCheckRetries++;
18089
+ state.iterHallucinationRetry = true;
18090
+ state.suppressRepetitionRetry = true;
18091
+ const reason = t("hall.claim_contradiction", {
18092
+ tool: contradiction.tool,
18093
+ snippet: contradiction.snippet
18094
+ });
18095
+ logger2.warn(`Success-claim contradiction: ${contradiction.tool}: ${contradiction.snippet}`);
18096
+ if (textContent) {
18097
+ deps.contextManager.addMessage({ role: "assistant", content: textContent });
18098
+ }
18099
+ deps.contextManager.addMessage({
18100
+ role: "user",
18101
+ content: `<system-summary>${reason} Original task: "${deps.input}".</system-summary>`
18102
+ });
18103
+ return { action: "retry" };
18104
+ }
17827
18105
  displayWarnLine(reason, deps) {
17828
18106
  const warnLine = `${t("hall.uncertainty_prefix").trim()} ${reason ?? ""}`;
17829
18107
  if (deps.onMeta) {
@@ -17840,6 +18118,7 @@ ${warnLine}
17840
18118
  var init_hallucination_gate = __esm(() => {
17841
18119
  init_colors();
17842
18120
  init_i18n();
18121
+ init_success_claim();
17843
18122
  });
17844
18123
 
17845
18124
  // src/core/agent/audit-gate.ts
@@ -24709,6 +24988,7 @@ function registerAllTools(registry2, skillsModule, opts) {
24709
24988
  processListTool,
24710
24989
  processLogTool,
24711
24990
  processKillTool,
24991
+ processWaitTool,
24712
24992
  webSearchTool,
24713
24993
  webFetchTool,
24714
24994
  webBrowseTool,
@@ -24746,6 +25026,7 @@ var init_tools = __esm(() => {
24746
25026
  init_process_list();
24747
25027
  init_process_log();
24748
25028
  init_process_kill();
25029
+ init_process_wait();
24749
25030
  init_subagent();
24750
25031
  init_chunk_query2();
24751
25032
  init_web_search();
@@ -32730,7 +33011,7 @@ function roleHeading(role) {
32730
33011
  return role;
32731
33012
  }
32732
33013
  }
32733
- function renderSessionMarkdown(meta, messages) {
33014
+ function renderSessionMarkdown(meta, messages, opts) {
32734
33015
  const lines = [];
32735
33016
  const name = meta?.name ?? "session";
32736
33017
  const id = meta?.id ?? "unknown";
@@ -32748,38 +33029,45 @@ function renderSessionMarkdown(meta, messages) {
32748
33029
  lines.push("---");
32749
33030
  lines.push("");
32750
33031
  for (const msg of messages) {
32751
- const heading = roleHeading(msg.role);
32752
- const nameSuffix = msg.name ? ` (${msg.name})` : "";
32753
- lines.push(`## ${heading}${nameSuffix}`);
32754
- lines.push("");
32755
- const text = getMessageText(msg.content);
32756
- lines.push(text.length > 0 ? text : "_(empty)_");
32757
- lines.push("");
32758
- if (Array.isArray(msg.tool_calls)) {
32759
- for (const tc of msg.tool_calls) {
32760
- lines.push(`- tool_call: \`${tc.function.name}\` ${tc.function.arguments}`);
32761
- }
32762
- lines.push("");
32763
- }
33032
+ appendMessageLines(lines, msg, opts);
32764
33033
  }
32765
33034
  return lines.join(`
32766
33035
  `);
32767
33036
  }
32768
- function buildSessionJson(meta, messages) {
32769
- return JSON.stringify({ meta, messages }, null, 2);
33037
+ function appendMessageLines(lines, msg, opts) {
33038
+ if (opts?.compact && msg.role === "tool")
33039
+ return;
33040
+ const heading = roleHeading(msg.role);
33041
+ const nameSuffix = msg.name ? ` (${msg.name})` : "";
33042
+ lines.push(`## ${heading}${nameSuffix}`);
33043
+ lines.push("");
33044
+ const text = getMessageText(msg.content);
33045
+ lines.push(text.length > 0 ? text : "_(empty)_");
33046
+ lines.push("");
33047
+ if (Array.isArray(msg.tool_calls)) {
33048
+ for (const tc of msg.tool_calls) {
33049
+ lines.push(`- tool_call: \`${tc.function.name}\` ${tc.function.arguments}`);
33050
+ }
33051
+ lines.push("");
33052
+ }
33053
+ }
33054
+ function buildSessionJson(meta, messages, opts) {
33055
+ const list = opts?.compact ? messages.filter((m) => m.role !== "tool") : messages;
33056
+ return JSON.stringify({ meta, messages: list }, null, 2);
32770
33057
  }
32771
- function buildSessionJsonl(log2) {
32772
- return log2.map((e) => JSON.stringify(e)).join(`
33058
+ function buildSessionJsonl(log2, opts) {
33059
+ const list = opts?.compact ? log2.filter((e) => !COMPACT_JSONL_DROP.has(e.type)) : log2;
33060
+ return list.map((e) => JSON.stringify(e)).join(`
32773
33061
  `);
32774
33062
  }
32775
- function formatExport(bundle, format) {
33063
+ function formatExport(bundle, format, opts) {
32776
33064
  switch (format) {
32777
33065
  case "md":
32778
- return renderSessionMarkdown(bundle.meta, bundle.messages);
33066
+ return renderSessionMarkdown(bundle.meta, bundle.messages, opts);
32779
33067
  case "json":
32780
- return buildSessionJson(bundle.meta, bundle.messages);
33068
+ return buildSessionJson(bundle.meta, bundle.messages, opts);
32781
33069
  case "jsonl":
32782
- return buildSessionJsonl(bundle.log);
33070
+ return buildSessionJsonl(bundle.log, opts);
32783
33071
  }
32784
33072
  }
32785
33073
  function redactExport(text, redact) {
@@ -32792,8 +33080,10 @@ function sanitizeFileName(name) {
32792
33080
  function extensionFor(format) {
32793
33081
  return format === "jsonl" ? "jsonl" : format;
32794
33082
  }
33083
+ var COMPACT_JSONL_DROP;
32795
33084
  var init_export = __esm(() => {
32796
33085
  init_context_log();
33086
+ COMPACT_JSONL_DROP = new Set(["context", "tool_defs", "tool_result", "llm_usage"]);
32797
33087
  });
32798
33088
 
32799
33089
  // src/modules/providers/naming.ts
@@ -41593,7 +41883,8 @@ var init_theme = __esm(() => {
41593
41883
  "recall",
41594
41884
  "session_info",
41595
41885
  "process_list",
41596
- "process_log"
41886
+ "process_log",
41887
+ "process_wait"
41597
41888
  ]),
41598
41889
  mutate: new Set([
41599
41890
  "write_file",
@@ -43041,6 +43332,7 @@ function registerSessionCommands(ctx) {
43041
43332
  let format = "md";
43042
43333
  let out;
43043
43334
  let redact = true;
43335
+ let compact = false;
43044
43336
  const positional = [];
43045
43337
  for (let i = 0;i < args.length; i++) {
43046
43338
  const a = args[i];
@@ -43060,6 +43352,8 @@ function registerSessionCommands(ctx) {
43060
43352
  out = v;
43061
43353
  } else if (a === "--no-redact") {
43062
43354
  redact = false;
43355
+ } else if (a === "--compact") {
43356
+ compact = true;
43063
43357
  } else {
43064
43358
  positional.push(a);
43065
43359
  }
@@ -43081,7 +43375,7 @@ function registerSessionCommands(ctx) {
43081
43375
  ctx.output.writeLine(pc.yellow(t("export.empty")));
43082
43376
  return;
43083
43377
  }
43084
- const text = redactExport(formatExport({ meta: match, messages, log: log2 }, format), redact);
43378
+ const text = redactExport(formatExport({ meta: match, messages, log: log2 }, format, { compact }), redact);
43085
43379
  if (out === "-") {
43086
43380
  ctx.output.writeLine(text);
43087
43381
  return;
@@ -44507,7 +44801,7 @@ function buildSessionCommands(program2) {
44507
44801
  }
44508
44802
  }
44509
44803
  });
44510
- session2.command("export").argument("<id>", "Session id (or name prefix)").description(t("cli.export_session")).option("--format <format>", t("cli.export_format"), "md").option("--out <path>", t("cli.export_out")).option("--no-redact", t("cli.export_no_redact")).action(async (id, opts) => {
44804
+ session2.command("export").argument("<id>", "Session id (or name prefix)").description(t("cli.export_session")).option("--format <format>", t("cli.export_format"), "md").option("--out <path>", t("cli.export_out")).option("--no-redact", t("cli.export_no_redact")).option("--compact", t("cli.export_compact")).action(async (id, opts) => {
44511
44805
  const { sessionManager } = await bootstrap();
44512
44806
  const meta = sessionManager.get(id) ?? sessionManager.list().find((s) => s.id.startsWith(id)) ?? sessionManager.list().find((s) => s.name.toLowerCase().includes(id.toLowerCase()));
44513
44807
  if (!meta) {
@@ -44525,7 +44819,7 @@ function buildSessionCommands(program2) {
44525
44819
  getDefaultChannel().writeLine(t("export.empty"));
44526
44820
  return;
44527
44821
  }
44528
- const text = redactExport(formatExport({ meta, messages, log: log2 }, format), opts.redact);
44822
+ const text = redactExport(formatExport({ meta, messages, log: log2 }, format, { compact: opts.compact }), opts.redact);
44529
44823
  if (opts.out === "-") {
44530
44824
  getDefaultChannel().writeLine(text);
44531
44825
  return;
@@ -46400,6 +46694,9 @@ class Renderer {
46400
46694
  thinkingLabel = "";
46401
46695
  textStarted = false;
46402
46696
  reconBuffer = [];
46697
+ pendingFooter = null;
46698
+ repeatCount = 0;
46699
+ repeatMs = 0;
46403
46700
  outputLineCount = 0;
46404
46701
  outputSuppressed = false;
46405
46702
  constructor(opts = {}) {
@@ -46548,6 +46845,7 @@ ${pc.dim(glyph.detail)} ${pc.cyan(stepContext)}
46548
46845
  this.verbosity = verbosity;
46549
46846
  }
46550
46847
  flushRecon() {
46848
+ this.flushPendingFooter();
46551
46849
  if (this.reconBuffer.length === 0)
46552
46850
  return;
46553
46851
  const items = this.reconBuffer;
@@ -46563,6 +46861,20 @@ ${pc.dim(glyph.detail)} ${pc.cyan(stepContext)}
46563
46861
  const total = items.reduce((a, b) => a + b.duration, 0);
46564
46862
  const label = t("ui.actions_summary", { count: String(items.length) });
46565
46863
  this.channel.writeRaw(`${pc.dim(glyph.group)} ${label} ${successMark()} ${pc.dim(`· ${formatDuration(total)}`)} ${pc.dim("· /verbose")}
46864
+ `);
46865
+ }
46866
+ flushPendingFooter() {
46867
+ const p = this.pendingFooter;
46868
+ if (!p)
46869
+ return;
46870
+ this.pendingFooter = null;
46871
+ const times = this.repeatCount + 1;
46872
+ const total = p.duration + this.repeatMs;
46873
+ this.repeatCount = 0;
46874
+ this.repeatMs = 0;
46875
+ const head = `${p.leadMark} ${toolAccent(p.tool)(p.label)}${p.summary ? ` ${pc.dim("·")} ${pc.dim(p.summary)}` : ""}`;
46876
+ const timesPart = times > 1 ? ` ${pc.dim(`×${times}`)}` : "";
46877
+ this.channel.writeRaw(`${head} ${successMark()} ${pc.dim(formatDuration(total))}${timesPart}
46566
46878
  `);
46567
46879
  }
46568
46880
  planBlock(lines) {
@@ -46612,28 +46924,7 @@ ${pc.dim(glyph.detail)} ${pc.cyan(stepContext)}
46612
46924
  tail += ` ${pc.dim("cost")} ${pc.yellow(formatUsd(costUsd))}`;
46613
46925
  }
46614
46926
  if (deferred) {
46615
- if (!error && RECON_TOOLS.has(this.card.tool)) {
46616
- this.reconBuffer.push({
46617
- tool: this.card.tool,
46618
- mark: leadMark,
46619
- label,
46620
- summary,
46621
- duration
46622
- });
46623
- this.recordLast(label, summary, error, duration);
46624
- this.card = null;
46625
- return;
46626
- }
46627
- this.flushRecon();
46628
- const head = `${leadMark} ${toolAccent(this.card.tool)(label)}${summary ? ` ${pc.dim("·")} ${pc.dim(summary)}` : ""}`;
46629
- this.channel.writeRaw(`${head} ${tail}
46630
- `);
46631
- if (error && firstBody) {
46632
- const soft = firstBody.startsWith("⚠");
46633
- const color = soft ? pc.yellow : pc.red;
46634
- this.channel.writeRaw(`${GUTTER}${pc.dim(glyph.detail)} ${color(firstBody)}
46635
- `);
46636
- }
46927
+ this.toolEndDeferred(tail, duration, error);
46637
46928
  } else {
46638
46929
  const footer = `${GUTTER}${tail}`;
46639
46930
  this.channel.writeRaw(`${footer}
@@ -46646,6 +46937,45 @@ ${pc.dim(glyph.detail)} ${pc.cyan(stepContext)}
46646
46937
  this.recordLast(label, summary, error, duration);
46647
46938
  this.card = null;
46648
46939
  }
46940
+ toolEndDeferred(tail, duration, error) {
46941
+ const card = this.card;
46942
+ if (!card)
46943
+ return;
46944
+ const { label, summary, mark: leadMark, firstBody } = card;
46945
+ if (!error && RECON_TOOLS.has(card.tool)) {
46946
+ this.flushPendingFooter();
46947
+ this.reconBuffer.push({ tool: card.tool, mark: leadMark, label, summary, duration });
46948
+ return;
46949
+ }
46950
+ const repeatKey = `${card.tool}::${label}::${summary}`;
46951
+ if (!error && this.pendingFooter?.key === repeatKey) {
46952
+ this.repeatCount += 1;
46953
+ this.repeatMs += duration;
46954
+ return;
46955
+ }
46956
+ this.flushPendingFooter();
46957
+ this.flushRecon();
46958
+ if (!error) {
46959
+ this.pendingFooter = {
46960
+ key: repeatKey,
46961
+ leadMark,
46962
+ tool: card.tool,
46963
+ label,
46964
+ summary,
46965
+ duration
46966
+ };
46967
+ return;
46968
+ }
46969
+ const head = `${leadMark} ${toolAccent(card.tool)(label)}${summary ? ` ${pc.dim("·")} ${pc.dim(summary)}` : ""}`;
46970
+ this.channel.writeRaw(`${head} ${tail}
46971
+ `);
46972
+ if (error && firstBody) {
46973
+ const soft = firstBody.startsWith("⚠");
46974
+ const color = soft ? pc.yellow : pc.red;
46975
+ this.channel.writeRaw(`${GUTTER}${pc.dim(glyph.detail)} ${color(firstBody)}
46976
+ `);
46977
+ }
46978
+ }
46649
46979
  recordLast(label, summary, error, duration) {
46650
46980
  const firstBody = this.card?.firstBody ?? "";
46651
46981
  const status = !error ? "ok" : firstBody.startsWith("⚠") ? "warn" : "error";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
5
5
  "type": "module",
6
6
  "bin": {