micro-models-agent 0.63.0 → 0.63.3

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,33 @@ 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
+ ## [0.63.3] - 2026-09-17
8
+
9
+ ### Fixed
10
+ - **Plan-alignment false positives hard-blocked legitimate writes**: `checkPlanAlignment` (`src/modules/execution/module.ts`) could accumulate 3 "outside the current step" warnings and block a tool call the agent needed for the current step (observed in `ses_mu5wed4r`: the current step's own `src/main.ts` was blocked). Causes fixed:
11
+ - **Path separators were not normalized** (root cause): model tool calls pass absolute Windows paths (`C:\…\src\main.ts`) while plan steps name files relatively (`src/main.ts`); the raw substring comparison never intersected backslash vs forward slash, so *every* write was treated as off-path. Both sides now go through `planPathToken` (forward slashes + lowercase) before matching.
12
+ - The completed-step rewrite exemption compared paths one-directionally (`finishedPaths.some((s) => s.includes(p))`), so a done step naming `main.ts` did not exempt a rewrite of `src/main.ts` — the overlap check is now bidirectional, mirroring the current-step comparison.
13
+ - Read-only diagnostic/structural tools were not exempt: `lsp_check` (mandated by the system prompt after every write) and `project_map` now join the allow-list (`PLAN_ALIGNMENT_EXEMPT_TOOLS`), alongside the other filesystem-reads, web/memory/session reads, process introspection and interactive prompts. Only file-mutating tools are pinned to the current step's file tokens.
14
+ - **Plugin-blocked calls were treated as real tool failures**: a guard refusal (`onBeforeTool` → `false`/string) is now flagged `blocked` on `ToolResult` and propagated to the context message (`src/tools/executor.ts`, `src/core/agent/tool-batch.ts`, `src/modules/context/manager.ts`). Blocked calls no longer feed the compaction "Already tried & failed — do NOT repeat" memory, the per-tool repeated-failure rule, or the consecutive-failure recovery counter — so a transient guard (or a guard bug) cannot convince the agent to never retry a correct action.
15
+
16
+ ### Added
17
+ - **Browser launch overrides**: `MMA_BROWSER_EXECUTABLE` (absolute path) and `MMA_BROWSER_CHANNEL` (`chrome`/`msedge`/`chromium`) let the browser tool use an installed system browser instead of Playwright's pinned Chromium. When the bundled revision is missing (offline / blocked CDN) it now falls back to `chrome`, then `msedge`, automatically (`src/modules/browser/driver.ts`, `bridge-server.mjs`).
18
+
19
+ ## [0.63.2] - 2026-09-17
20
+
21
+ ### Fixed
22
+ - **Long answers were silently cut by the completion cap**: `max_tokens` (default 4096) is sent on every request; when the model hit it while already streaming text, `finish_reason: "length"` was ignored and the truncated text was accepted as the final answer. The provider now emits a non-fatal `warning` chunk (`src/llm/openai-compat.ts`, streaming and non-streaming paths) that the agent renders in the chat — the answer still arrives, but the user sees it was cut off. Tool-call/empty-response truncation keeps the existing recoverable-error path.
23
+ - **Resumed sessions lost their history**: `saveAssistantMessage` and `logToolResult` (`src/core/session-logger.ts`) persisted only the first 500 chars of each assistant/tool message, so `/resume` seeded the model with truncated context. Full content is stored now; the context budget still trims what actually goes to the LLM.
24
+
25
+ ## [0.63.1] - 2026-09-17
26
+
27
+ ### Added
28
+ - **`ui.verbose`**: opt-in chatty mode for internal orchestration status. MoE planning / re-plan / execution / scope one-liners (`src/core/agent-moe.ts`) are now behind this flag (default `false`) instead of always being written to the chat.
29
+
30
+ ### Changed
31
+ - **Failed tool output is no longer echoed verbatim**: the chat used to print the full model-facing `result.output` of any tool without a `display` — including `Hint:` / recovery directives / "Do NOT …" instructions written for the agent. Only the first non-empty line (the gist of the error) is shown now, in red (`compactToolError`, `src/core/agent/tool-output.ts`).
32
+ - **Live busy spinners for tool runs**: the renderer animates while `read_file` / `write_file` / `edit_file` / `bash` / … execute and restarts the spinner on every LLM "thinking" phase. Previously the spinner stopped at the first tool call and the rest of the run looked frozen. Interactive tools (`question`, `approve`) never spin; dead `BUSY_TOOLS` set removed.
33
+
7
34
  ## [0.63.0] - 2026-09-17
8
35
 
9
36
  ### Added
package/dist/i18n/en.json CHANGED
@@ -44,6 +44,7 @@
44
44
  "error.llm_stream_idle_toolcall": "LLM stream stalled after a tool_call started — no data for {timeout}ms. The provider most likely buffers SSE instead of streaming tool-call argument deltas (seen with LM Studio). Raise \"retry.noDataTimeoutMs\" in ~/.mma/config.json (restart required)",
45
45
  "error.llm_truncated": "Response hit the completion token limit ({tokens}) and was cut off before any content arrived. Split the task into smaller outputs or raise \"maxCompletionTokens\" in the config",
46
46
  "error.llm_truncated_toolcall": "Response hit the completion token limit ({tokens}) in the middle of a tool_call — its arguments were cut off. Write the file in smaller chunks (several write_file/edit_file calls) or raise \"maxCompletionTokens\" in the config",
47
+ "error.llm_output_truncated": "The response hit the completion token limit ({tokens}) and was cut off — this answer is incomplete. Raise \"maxCompletionTokens\" in the config to allow longer replies",
47
48
  "error.llm_provider_stream_error": "Provider returned an error mid-stream: {error}",
48
49
  "error.llm_timeout": "LLM request timed out ({timeout}ms)",
49
50
  "env.runtime_node": "Running under Node (v{version}) — clipboard image paste, subagent performance and LSP spawn on Windows degrade. Install Bun (https://bun.sh) for full features.",
package/dist/i18n/ru.json CHANGED
@@ -43,6 +43,7 @@
43
43
  "error.llm_stream_idle_toolcall": "Поток LLM завис после начала tool_call — нет данных {timeout}мс. Скорее всего провайдер буферизирует SSE вместо потоковой передачи аргументов tool_call (наблюдается в LM Studio). Увеличьте \"retry.noDataTimeoutMs\" в ~/.mma/config.json (нужен рестарт)",
44
44
  "error.llm_truncated": "Ответ упёрся в лимит токенов генерации ({tokens}) и был обрезан до какого-либо содержимого. Разбейте задачу на меньшие порции вывода или увеличьте \"maxCompletionTokens\" в конфиге",
45
45
  "error.llm_truncated_toolcall": "Ответ упёрся в лимит токенов генерации ({tokens}) посреди tool_call — его аргументы обрезаны. Пишите файл частями (несколько вызовов write_file/edit_file) или увеличьте \"maxCompletionTokens\" в конфиге",
46
+ "error.llm_output_truncated": "Ответ упёрся в лимит токенов генерации ({tokens}) и был обрезан — этот ответ неполный. Увеличьте \"maxCompletionTokens\" в конфиге, чтобы разрешить длинные ответы",
46
47
  "error.llm_provider_stream_error": "Провайдер вернул ошибку посреди стрима: {error}",
47
48
  "error.llm_timeout": "Время запроса LLM истекло ({timeout}мс)",
48
49
  "env.runtime_node": "Запущено под Node (v{version}) — вставка изображений из буфера и LSP на Windows работают урезанно. Установите Bun (https://bun.sh) для полного функционала.",
package/dist/main.js CHANGED
@@ -2373,7 +2373,8 @@ var init_defaults = __esm(() => {
2373
2373
  ui: {
2374
2374
  spinner: true,
2375
2375
  showContextStats: false,
2376
- showCompaction: true
2376
+ showCompaction: true,
2377
+ verbose: false
2377
2378
  },
2378
2379
  mcpServers: {
2379
2380
  context7: {
@@ -2484,6 +2485,7 @@ The path was joined onto the working directory because it does not exist as give
2484
2485
  "error.llm_stream_idle_toolcall": 'LLM stream stalled after a tool_call started — no data for {timeout}ms. The provider most likely buffers SSE instead of streaming tool-call argument deltas (seen with LM Studio). Raise "retry.noDataTimeoutMs" in ~/.mma/config.json (restart required)',
2485
2486
  "error.llm_truncated": 'Response hit the completion token limit ({tokens}) and was cut off before any content arrived. Split the task into smaller outputs or raise "maxCompletionTokens" in the config',
2486
2487
  "error.llm_truncated_toolcall": 'Response hit the completion token limit ({tokens}) in the middle of a tool_call — its arguments were cut off. Write the file in smaller chunks (several write_file/edit_file calls) or raise "maxCompletionTokens" in the config',
2488
+ "error.llm_output_truncated": 'The response hit the completion token limit ({tokens}) and was cut off — this answer is incomplete. Raise "maxCompletionTokens" in the config to allow longer replies',
2487
2489
  "error.llm_provider_stream_error": "Provider returned an error mid-stream: {error}",
2488
2490
  "error.llm_timeout": "LLM request timed out ({timeout}ms)",
2489
2491
  "env.runtime_node": "Running under Node (v{version}) — clipboard image paste, subagent performance and LSP spawn on Windows degrade. Install Bun (https://bun.sh) for full features.",
@@ -3320,6 +3322,7 @@ var init_ru = __esm(() => {
3320
3322
  "error.llm_stream_idle_toolcall": 'Поток LLM завис после начала tool_call — нет данных {timeout}мс. Скорее всего провайдер буферизирует SSE вместо потоковой передачи аргументов tool_call (наблюдается в LM Studio). Увеличьте "retry.noDataTimeoutMs" в ~/.mma/config.json (нужен рестарт)',
3321
3323
  "error.llm_truncated": 'Ответ упёрся в лимит токенов генерации ({tokens}) и был обрезан до какого-либо содержимого. Разбейте задачу на меньшие порции вывода или увеличьте "maxCompletionTokens" в конфиге',
3322
3324
  "error.llm_truncated_toolcall": 'Ответ упёрся в лимит токенов генерации ({tokens}) посреди tool_call — его аргументы обрезаны. Пишите файл частями (несколько вызовов write_file/edit_file) или увеличьте "maxCompletionTokens" в конфиге',
3325
+ "error.llm_output_truncated": 'Ответ упёрся в лимит токенов генерации ({tokens}) и был обрезан — этот ответ неполный. Увеличьте "maxCompletionTokens" в конфиге, чтобы разрешить длинные ответы',
3323
3326
  "error.llm_provider_stream_error": "Провайдер вернул ошибку посреди стрима: {error}",
3324
3327
  "error.llm_timeout": "Время запроса LLM истекло ({timeout}мс)",
3325
3328
  "env.runtime_node": "Запущено под Node (v{version}) — вставка изображений из буфера и LSP на Windows работают урезанно. Установите Bun (https://bun.sh) для полного функционала.",
@@ -6361,6 +6364,10 @@ class OpenAICompatProvider {
6361
6364
  tokens: maxTokens
6362
6365
  }), { terminal: true, recoverable: true });
6363
6366
  }
6367
+ yield {
6368
+ type: "warning",
6369
+ content: t("error.llm_output_truncated", { tokens: maxTokens })
6370
+ };
6364
6371
  }
6365
6372
  } finally {
6366
6373
  cleanup();
@@ -6420,12 +6427,13 @@ class OpenAICompatProvider {
6420
6427
  };
6421
6428
  }
6422
6429
  async doNonStreaming(messages, tools, signal, options) {
6430
+ const maxTokens = options?.maxTokens ?? this.config.maxCompletionTokens ?? 4096;
6423
6431
  const body = buildRequestBody({
6424
6432
  model: this.model,
6425
6433
  messages,
6426
6434
  tools,
6427
6435
  stream: false,
6428
- maxTokens: options?.maxTokens ?? this.config.maxCompletionTokens ?? 4096,
6436
+ maxTokens,
6429
6437
  reasoningEffort: options?.reasoningEffort,
6430
6438
  reasoningStrategy: options?.reasoningStrategy,
6431
6439
  ...this.cacheHints()
@@ -6475,6 +6483,12 @@ class OpenAICompatProvider {
6475
6483
  });
6476
6484
  }
6477
6485
  }
6486
+ if (choice.finish_reason === "length" && msg.content && !msg.tool_calls) {
6487
+ chunks.push({
6488
+ type: "warning",
6489
+ content: t("error.llm_output_truncated", { tokens: maxTokens })
6490
+ });
6491
+ }
6478
6492
  if (data.usage) {
6479
6493
  chunks.push({
6480
6494
  type: "done",
@@ -7285,6 +7299,7 @@ class ToolExecutor {
7285
7299
  const reason = typeof proceed === "string" ? proceed : undefined;
7286
7300
  return {
7287
7301
  success: false,
7302
+ blocked: true,
7288
7303
  output: reason ? t("tool.blocked_reason", { plugin: pluginName, reason }) : t("tool.blocked", { plugin: pluginName }),
7289
7304
  toolCallId: call.id
7290
7305
  };
@@ -10419,7 +10434,7 @@ class SessionLogger {
10419
10434
  const caller = this.getCaller?.();
10420
10435
  this.session?.appendMessage({
10421
10436
  role: "assistant",
10422
- content: content.slice(0, 500),
10437
+ content,
10423
10438
  timestamp: new Date().toISOString(),
10424
10439
  provider: caller?.provider,
10425
10440
  model: caller?.model
@@ -10448,7 +10463,7 @@ class SessionLogger {
10448
10463
  const caller = this.getCaller?.();
10449
10464
  this.session?.appendMessage({
10450
10465
  role: "tool",
10451
- content: sanitizeLogMessage(result.output.slice(0, 500)),
10466
+ content: sanitizeLogMessage(result.output),
10452
10467
  name: call.name,
10453
10468
  timestamp: new Date().toISOString(),
10454
10469
  provider: caller?.provider,
@@ -13892,6 +13907,11 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13892
13907
  if (signal?.aborted)
13893
13908
  throw newAbortError();
13894
13909
  };
13910
+ const verbose = config.ui?.verbose === true;
13911
+ const status = (message) => {
13912
+ if (verbose)
13913
+ onMeta?.(message);
13914
+ };
13895
13915
  ensureNotAborted();
13896
13916
  let orchestrator = deps.orchestratorOverride;
13897
13917
  if (!orchestrator) {
@@ -13909,7 +13929,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13909
13929
  logger.warn("MoE enabled but no orchestrator model configured — falling back to single-agent. Set orchestrator.model to activate MoE.");
13910
13930
  return fallback();
13911
13931
  }
13912
- onMeta?.(`\uD83E\uDD16 Planning with MoE mode...
13932
+ status(`\uD83E\uDD16 Planning with MoE mode...
13913
13933
  `);
13914
13934
  onPhase?.("thinking");
13915
13935
  const planResult = await orchestrator.plan(input, undefined, signal);
@@ -13931,7 +13951,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13931
13951
  for (let cycle = 1;cycle <= maxCycles; cycle++) {
13932
13952
  ensureNotAborted();
13933
13953
  if (cycle > 1) {
13934
- onMeta?.(t("moe.replan_started", { cycle, max: maxCycles }) + `
13954
+ status(t("moe.replan_started", { cycle, max: maxCycles }) + `
13935
13955
  `);
13936
13956
  emit("moe_replan", {
13937
13957
  cycle,
@@ -13946,7 +13966,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13946
13966
  if (!retry.valid) {
13947
13967
  logger.warn(`MoE plan validation failed: ${retry.errors.join("; ")}`);
13948
13968
  if (cycle === 1) {
13949
- onMeta?.(`⚠️ Plan validation failed. Falling back to single-agent mode.
13969
+ status(`⚠️ Plan validation failed. Falling back to single-agent mode.
13950
13970
  `);
13951
13971
  return fallback();
13952
13972
  }
@@ -13954,7 +13974,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13954
13974
  }
13955
13975
  currentPlan = applied;
13956
13976
  if (validation.autoFixes.length > 0) {
13957
- onMeta?.(`\uD83D\uDD27 Auto-fixed ${validation.autoFixes.length} plan issues.
13977
+ status(`\uD83D\uDD27 Auto-fixed ${validation.autoFixes.length} plan issues.
13958
13978
  `);
13959
13979
  }
13960
13980
  }
@@ -13971,19 +13991,19 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
13971
13991
  onScopeRequest: async (subtaskId, req) => {
13972
13992
  const decision = await orchestrator.resolveScopeRequest(subtaskId, req, signal);
13973
13993
  if (decision.action === "approve") {
13974
- onMeta?.(t("moe.scope_approved", {
13994
+ status(t("moe.scope_approved", {
13975
13995
  subtask: subtaskId,
13976
13996
  files: [...decision.write, ...decision.read].join(", ")
13977
13997
  }) + `
13978
13998
  `);
13979
13999
  } else {
13980
- onMeta?.(t("moe.scope_rejected", { subtask: subtaskId }) + `
14000
+ status(t("moe.scope_rejected", { subtask: subtaskId }) + `
13981
14001
  `);
13982
14002
  }
13983
14003
  return decision;
13984
14004
  }
13985
14005
  });
13986
- onMeta?.(t("moe.executing", { count: String(currentPlan.subtasks.length), cycle: String(cycle) }) + `
14006
+ status(t("moe.executing", { count: String(currentPlan.subtasks.length), cycle: String(cycle) }) + `
13987
14007
  `);
13988
14008
  const skipIds = new Set;
13989
14009
  const carriedResults = [];
@@ -14019,7 +14039,7 @@ async function runWithMoE(deps, input, fallback, opts = {}) {
14019
14039
  mergedResults.set(r.subtaskId, r);
14020
14040
  }
14021
14041
  const succeeded = [...mergedResults.values()].filter((r) => r.success).length;
14022
- onMeta?.(`✅ ${t("moe.execution_complete", { succeeded: String(succeeded), total: String(mergedResults.size) })}
14042
+ status(`✅ ${t("moe.execution_complete", { succeeded: String(succeeded), total: String(mergedResults.size) })}
14023
14043
  `);
14024
14044
  const verifier = deps.verifierOverride ?? new StepVerifier(baseDir);
14025
14045
  const knownTags = collectKnownToolTags(toolExecutor);
@@ -14691,6 +14711,17 @@ function summarizeToolArgs(args) {
14691
14711
  return ` (${s})`;
14692
14712
  return ` (${s.slice(0, TOOL_ARGS_SUMMARY_MAX_CHARS)}…(${s.length} chars total))`;
14693
14713
  }
14714
+ function compactToolError(output, max = 200) {
14715
+ const text = String(output ?? "");
14716
+ for (const line of text.split(`
14717
+ `)) {
14718
+ const trimmed = line.trim();
14719
+ if (!trimmed)
14720
+ continue;
14721
+ return trimmed.length > max ? `${trimmed.slice(0, max - 1)}…` : trimmed;
14722
+ }
14723
+ return "";
14724
+ }
14694
14725
  var init_tool_output = __esm(() => {
14695
14726
  init_i18n();
14696
14727
  });
@@ -14766,7 +14797,7 @@ class ToolBatchExecutor {
14766
14797
  if (this.deps.isShutdownRequested())
14767
14798
  break;
14768
14799
  const duration = Date.now() - startTime;
14769
- if (!result.success && !infoTools.has(call.name))
14800
+ if (!result.success && !result.blocked && !infoTools.has(call.name))
14770
14801
  anyToolFailed = true;
14771
14802
  if (result.success && call.name === "plan") {
14772
14803
  const action = String(call.arguments.action ?? "");
@@ -14788,10 +14819,17 @@ class ToolBatchExecutor {
14788
14819
  ` + result.display + `
14789
14820
  `);
14790
14821
  }
14791
- } else {
14822
+ } else if (result.success) {
14792
14823
  const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, result.output);
14793
14824
  onMeta?.(`
14794
14825
  ` + pc2.dim(metaOut) + `
14826
+ `);
14827
+ } else {
14828
+ const line = compactToolError(result.output);
14829
+ const metaOut = line ? pluginManager.runOnMeta({ iteration, logger, contextManager }, line) : "";
14830
+ if (metaOut)
14831
+ onMeta?.(`
14832
+ ` + pc2.red(metaOut) + `
14795
14833
  `);
14796
14834
  }
14797
14835
  if (result.diff) {
@@ -14808,6 +14846,7 @@ class ToolBatchExecutor {
14808
14846
  name: call.name,
14809
14847
  tool_call_id: call.id,
14810
14848
  success: result.success,
14849
+ blocked: result.blocked,
14811
14850
  arguments: call.arguments
14812
14851
  });
14813
14852
  answeredToolCallIds.add(call.id);
@@ -14826,7 +14865,7 @@ class ToolBatchExecutor {
14826
14865
  slog?.logToolResult(call, result, duration, iteration);
14827
14866
  }
14828
14867
  this.deps.compactionService.compactAfterTool(state);
14829
- if (!result.success && !infoTools.has(call.name)) {
14868
+ if (!result.success && !result.blocked && !infoTools.has(call.name)) {
14830
14869
  const key = call.name;
14831
14870
  const prev = state.toolFailureCounts.get(key) ?? { count: 0, error: "" };
14832
14871
  prev.count++;
@@ -15697,6 +15736,13 @@ class Agent {
15697
15736
  if (chunk.type === "done" && chunk.usage) {
15698
15737
  tokenTracker.recordApiUsage(state, baseline, chunk.usage);
15699
15738
  }
15739
+ if (chunk.type === "warning" && chunk.content) {
15740
+ logger.warn(`LLM warning: ${chunk.content}`);
15741
+ onChunk?.(`
15742
+
15743
+ > ⚠️ ${chunk.content}
15744
+ `);
15745
+ }
15700
15746
  }
15701
15747
  } catch (err) {
15702
15748
  if (this.shutdownRequested || err?.name === "AbortError") {
@@ -16189,6 +16235,8 @@ function extractTriedAndFailed(messages) {
16189
16235
  const failures = new Map;
16190
16236
  for (const msg of messages) {
16191
16237
  if (msg.role === "tool" && msg.name && msg.success === false) {
16238
+ if (msg.blocked)
16239
+ continue;
16192
16240
  const key = `${msg.name}:${summarizeArgs(msg.arguments)}`;
16193
16241
  const existing = failures.get(key);
16194
16242
  const errorText = truncate(typeof msg.content === "string" ? msg.content : getMessageText(msg.content), 100);
@@ -19543,6 +19591,8 @@ class BridgeDriver {
19543
19591
  headless: cfg.headless,
19544
19592
  viewport: cfg.viewport,
19545
19593
  timeoutMs: cfg.timeoutMs,
19594
+ executablePath: cfg.overrides?.executablePath,
19595
+ channel: cfg.overrides?.channel,
19546
19596
  maxConsoleLineChars: this.maxConsoleLineChars
19547
19597
  });
19548
19598
  }
@@ -19665,6 +19715,41 @@ var init_bridge_client = __esm(() => {
19665
19715
 
19666
19716
  // src/modules/browser/driver.ts
19667
19717
  import { chromium } from "playwright";
19718
+ function resolveBrowserLaunchOverrides(env = process.env) {
19719
+ const executablePath = env.MMA_BROWSER_EXECUTABLE?.trim();
19720
+ const channel = env.MMA_BROWSER_CHANNEL?.trim();
19721
+ const out = {};
19722
+ if (executablePath)
19723
+ out.executablePath = executablePath;
19724
+ if (channel)
19725
+ out.channel = channel;
19726
+ return out;
19727
+ }
19728
+ function isMissingBrowserError(err) {
19729
+ return /Executable doesn't exist|is not found|looks like Playwright was just installed/i.test(String(err?.message ?? err));
19730
+ }
19731
+ async function launchChromiumWithFallback(launch, cfg, overrides = {}) {
19732
+ const base = { headless: cfg.headless, timeout: cfg.timeoutMs, ...overrides };
19733
+ try {
19734
+ return await launch(base);
19735
+ } catch (err) {
19736
+ const explicit = overrides.executablePath || overrides.channel;
19737
+ if (explicit || !isMissingBrowserError(err))
19738
+ throw err;
19739
+ let lastErr = err;
19740
+ for (const channel of SYSTEM_CHANNEL_FALLBACKS) {
19741
+ try {
19742
+ return await launch({ ...base, channel });
19743
+ } catch (e) {
19744
+ lastErr = e;
19745
+ }
19746
+ }
19747
+ throw lastErr;
19748
+ }
19749
+ }
19750
+ function launchChromium(cfg, overrides) {
19751
+ return launchChromiumWithFallback((opts) => chromium.launch(opts), cfg, overrides);
19752
+ }
19668
19753
 
19669
19754
  class PlaywrightDriver {
19670
19755
  browser = null;
@@ -19675,10 +19760,7 @@ class PlaywrightDriver {
19675
19760
  async launch(cfg) {
19676
19761
  if (this.browser)
19677
19762
  return;
19678
- this.browser = await chromium.launch({
19679
- headless: cfg.headless,
19680
- timeout: cfg.timeoutMs
19681
- });
19763
+ this.browser = await launchChromium(cfg, cfg.overrides ?? {});
19682
19764
  this.context = await this.browser.newContext({ viewport: cfg.viewport });
19683
19765
  this.page = await this.context.newPage();
19684
19766
  this.page.setDefaultTimeout(cfg.timeoutMs);
@@ -19767,7 +19849,8 @@ function createBrowserDriver(config) {
19767
19849
  const common = {
19768
19850
  headless: config.headless,
19769
19851
  viewport: { width: config.viewportWidth, height: config.viewportHeight },
19770
- timeoutMs: config.navigationTimeout
19852
+ timeoutMs: config.navigationTimeout,
19853
+ overrides: resolveBrowserLaunchOverrides()
19771
19854
  };
19772
19855
  return (async () => {
19773
19856
  if (process.versions.bun) {
@@ -19789,9 +19872,10 @@ async function launchBridge(config, common) {
19789
19872
  await bridge.launch(common);
19790
19873
  return bridge;
19791
19874
  }
19792
- var DIRECT_LAUNCH_TIMEOUT_MS = 8000;
19875
+ var DIRECT_LAUNCH_TIMEOUT_MS = 8000, SYSTEM_CHANNEL_FALLBACKS;
19793
19876
  var init_driver = __esm(() => {
19794
19877
  init_session();
19878
+ SYSTEM_CHANNEL_FALLBACKS = ["chrome", "msedge"];
19795
19879
  });
19796
19880
 
19797
19881
  // src/modules/browser/types.ts
@@ -22985,6 +23069,9 @@ var init_plan_tool = __esm(() => {
22985
23069
  // src/modules/execution/module.ts
22986
23070
  import { existsSync as existsSync39, readFileSync as readFileSync23 } from "fs";
22987
23071
  import { resolve as resolve18 } from "path";
23072
+ function planPathToken(p) {
23073
+ return toForwardSlash(p).toLowerCase();
23074
+ }
22988
23075
 
22989
23076
  class ExecutionModule {
22990
23077
  name = "execution";
@@ -23263,38 +23350,28 @@ class ExecutionModule {
23263
23350
  const step = this.tracker.getCurrentStep();
23264
23351
  if (!step)
23265
23352
  return null;
23266
- const allowedAlways = [
23267
- "plan",
23268
- "todo",
23269
- "verify",
23270
- "list_dir",
23271
- "read_file",
23272
- "glob",
23273
- "grep",
23274
- "file_info",
23275
- "load_skill"
23276
- ];
23277
- if (allowedAlways.includes(call.name))
23353
+ if (PLAN_ALIGNMENT_EXEMPT_TOOLS.has(call.name))
23278
23354
  return null;
23279
- const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map((p) => p.toLowerCase());
23355
+ const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map(planPathToken);
23280
23356
  if (stepPaths.length === 0)
23281
23357
  return null;
23282
23358
  const argStr = this.pathArgStrings(call.arguments).join(" ");
23283
- const callPaths = extractFileLikeTokens(stripUrls(argStr)).map((p) => p.toLowerCase());
23359
+ const callPaths = extractFileLikeTokens(stripUrls(argStr)).map(planPathToken);
23284
23360
  if (callPaths.length === 0)
23285
23361
  return null;
23286
23362
  const plan = this.tracker.getPlan();
23287
23363
  const finishedPaths = new Set;
23288
23364
  for (const s of plan.steps) {
23289
23365
  if (s.status === "done" || s.status === "skipped") {
23290
- extractFileLikeTokens(stripUrls(s.description)).map((p) => p.toLowerCase()).forEach((p) => finishedPaths.add(p));
23366
+ extractFileLikeTokens(stripUrls(s.description)).map(planPathToken).forEach((p) => finishedPaths.add(p));
23291
23367
  }
23292
23368
  }
23369
+ const matchesAny = (p, tokens) => tokens.some((t2) => t2 === p || t2.includes(p) || p.includes(t2));
23370
+ const finished = [...finishedPaths];
23293
23371
  const offPath = callPaths.some((p) => {
23294
- if (finishedPaths.has(p) || [...finishedPaths].some((s) => s.includes(p))) {
23372
+ if (matchesAny(p, finished))
23295
23373
  return false;
23296
- }
23297
- return !stepPaths.some((s) => p.includes(s) || s.includes(p));
23374
+ return !matchesAny(p, stepPaths);
23298
23375
  });
23299
23376
  if (!offPath)
23300
23377
  return null;
@@ -23466,7 +23543,7 @@ class ExecutionModule {
23466
23543
  return getMessageText(firstUser.content).trim();
23467
23544
  }
23468
23545
  }
23469
- var ERROR_SEARCH_MIN_INTERVAL_MS = 30000;
23546
+ var ERROR_SEARCH_MIN_INTERVAL_MS = 30000, PLAN_ALIGNMENT_EXEMPT_TOOLS;
23470
23547
  var init_module = __esm(() => {
23471
23548
  init_i18n();
23472
23549
  init_tracker();
@@ -23475,10 +23552,36 @@ var init_module = __esm(() => {
23475
23552
  init_auditor();
23476
23553
  init_plan_store();
23477
23554
  init_js_identifiers();
23555
+ init_path_utils();
23478
23556
  init_web_search();
23479
23557
  init_session_isolation();
23480
23558
  init_plan_tool();
23481
23559
  init_execution_plugin();
23560
+ PLAN_ALIGNMENT_EXEMPT_TOOLS = new Set([
23561
+ "plan",
23562
+ "todo",
23563
+ "verify",
23564
+ "enable_tools",
23565
+ "list_dir",
23566
+ "read_file",
23567
+ "glob",
23568
+ "grep",
23569
+ "file_info",
23570
+ "lsp_check",
23571
+ "project_map",
23572
+ "chunk_query",
23573
+ "web_search",
23574
+ "web_fetch",
23575
+ "web_browse",
23576
+ "recall",
23577
+ "search_history",
23578
+ "session_info",
23579
+ "process_list",
23580
+ "process_log",
23581
+ "question",
23582
+ "approve",
23583
+ "load_skill"
23584
+ ]);
23482
23585
  });
23483
23586
 
23484
23587
  // src/modules/security/session-encryption.ts
@@ -39921,7 +40024,7 @@ function toDisplayPath(baseDir, p) {
39921
40024
  }
39922
40025
  var GUTTER = " ";
39923
40026
  var MAX_OUTPUT_LINES = 50;
39924
- var BUSY_TOOLS = new Set(["lsp_check"]);
40027
+ var SPINNERLESS_TOOLS = new Set(["question", "approve"]);
39925
40028
  function toolMarker(tool) {
39926
40029
  switch (tool) {
39927
40030
  case "write_file":
@@ -39995,7 +40098,7 @@ class Renderer {
39995
40098
  `), this.width, (text) => this.out.write(text));
39996
40099
  }
39997
40100
  showLoader() {
39998
- this.spinner.start("thinking");
40101
+ this.spinner.start(t("ui.thinking"));
39999
40102
  }
40000
40103
  text(chunk) {
40001
40104
  this.endCard();
@@ -40049,6 +40152,7 @@ ${pc2.dim("→")} Thought: `);
40049
40152
  this.thoughtStarted = true;
40050
40153
  this.thoughtStartMs = Date.now();
40051
40154
  this.thoughtHeaderPrinted = false;
40155
+ this.spinner.start(t("ui.thinking"));
40052
40156
  }
40053
40157
  thinkingEnd() {
40054
40158
  this.spinner.stop();
@@ -40068,14 +40172,18 @@ ${pc2.dim("→")} Thought: `);
40068
40172
  const displayArgs = PATH_TOOLS.has(tool) && typeof args.path === "string" ? { ...args, path: toDisplayPath(this.baseDir, args.path) } : args;
40069
40173
  const summary = summarizeArgs2(displayArgs);
40070
40174
  const marker = icon || toolMarker(tool);
40175
+ const label = friendlyTool(tool);
40071
40176
  this.card = { tool, args, start: Date.now() };
40072
40177
  if (stepContext) {
40073
40178
  this.out.write(`
40074
40179
  ${pc2.dim("↓")} ${pc2.cyan(stepContext)}
40075
40180
  `);
40076
40181
  }
40077
- this.out.write(`${pc2.dim(marker)} ${friendlyTool(tool)}${summary ? ` ${pc2.dim(summary)}` : ""}
40182
+ this.out.write(`${pc2.dim(marker)} ${label}${summary ? ` ${pc2.dim(summary)}` : ""}
40078
40183
  `);
40184
+ if (!SPINNERLESS_TOOLS.has(tool)) {
40185
+ this.spinner.start(`${label}${summary ? ` ${summary}` : ""}`);
40186
+ }
40079
40187
  }
40080
40188
  planBlock(lines) {
40081
40189
  this.endCard();
@@ -41,13 +41,46 @@ function readInt(v, fallback) {
41
41
  return Number.isFinite(n) && n > 0 ? n : fallback;
42
42
  }
43
43
 
44
+ // System browsers to try when the bundled Chromium revision is absent (CDN
45
+ // blocked / offline). Mirrors driver.ts so both transports behave the same.
46
+ const SYSTEM_CHANNEL_FALLBACKS = ["chrome", "msedge"];
47
+
48
+ function isMissingBrowserError(err) {
49
+ return /Executable doesn't exist|is not found|looks like Playwright was just installed/i.test(
50
+ String(err && err.message ? err.message : err),
51
+ );
52
+ }
53
+
54
+ async function launchChromium(params, extra) {
55
+ const base = {
56
+ headless: params?.headless !== false,
57
+ timeout: readInt(params?.timeoutMs, 30000),
58
+ ...extra,
59
+ };
60
+ try {
61
+ return await chromium.launch(base);
62
+ } catch (err) {
63
+ // An explicit executable/channel is the user's choice — fail loudly.
64
+ if (base.executablePath || base.channel || !isMissingBrowserError(err)) throw err;
65
+ let lastErr = err;
66
+ for (const channel of SYSTEM_CHANNEL_FALLBACKS) {
67
+ try {
68
+ return await chromium.launch({ ...base, channel });
69
+ } catch (e) {
70
+ lastErr = e;
71
+ }
72
+ }
73
+ throw lastErr;
74
+ }
75
+ }
76
+
44
77
  async function handleLaunch(params) {
45
78
  if (browser) return { ok: true };
46
79
  maxLineChars = readInt(params?.maxConsoleLineChars, 400);
47
- browser = await chromium.launch({
48
- headless: params?.headless !== false,
49
- timeout: readInt(params?.timeoutMs, 30000),
50
- });
80
+ const extra = {};
81
+ if (params?.executablePath) extra.executablePath = String(params.executablePath);
82
+ if (params?.channel) extra.channel = String(params.channel);
83
+ browser = await launchChromium(params, extra);
51
84
  context = await browser.newContext({
52
85
  viewport: params?.viewport || { width: 1280, height: 720 },
53
86
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.63.0",
3
+ "version": "0.63.3",
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": {