micro-models-agent 2.14.0 → 2.15.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
@@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.15.0] - 2026-10-01
10
+
11
+ ### Added
12
+
13
+ - **Web UI: stale-translations badge** (`src/modules/webui/i18n-stale.ts`, `i18n-api.ts`, `host-state.ts`, `core/host.ts`, `assets/index.html`, `js/dom.js`, `js/settings.js`, `app.js`, `styles.css`): `GET /api/i18n` and the state snapshot carry `i18nStale` (dict files on disk newer than the server process); the topbar shows a yellow badge with a reload action instead of silently rendering raw keys. New keys `web.i18n.stale`/`web.i18n.reload` (en+ru). Tests: `tests/webui-i18n.test.ts`, `tests/webui-usage.test.ts`, `tests/webui-assets.test.ts`.
14
+
15
+ ### Fixed
16
+
17
+ - **Plan auto-advance no longer completes modify-steps on stale files** (`src/modules/execution/module.ts`): `advancePlanIfStepComplete` and final-audit `autoCloseEvidenceSteps` now require `provablyStale()` to be false — every named file must be created/modified after `plan.createdAt` (fail-open on bad dates and unreadable mtimes, delete-kind unchanged). Previously a step naming a long-existing file auto-completed before a line of code was written (observed: 6 plan steps auto-closed while the agent was still on step 1). Tests: `tests/execution-plan-deliverables.test.ts`.
18
+ - **Critic evidence includes git-tracked changes** (`src/modules/critic/evidence.ts`): `buildEvidencePack` unions tool-arg file lists with `git status --porcelain` (staged + unstaged + untracked, capped, fail-open outside repos), so edits made outside file tools (helper scripts via bash, heredocs) no longer produce false `concern` verdicts about missing changes. Tests: `tests/critic-evidence.test.ts`.
19
+ - **Repeated-failure memory writes are diagnosed, model misses skipped** (`src/core/agent/tool-batch.ts`, `loop-state.ts`): `diagnoseRepeatedFailures()` records distinct error headlines with signature-based cause/solution (edit_file anchor miss → CRLF/stale-read hypothesis) and skips global-memory writes for model errors (invented test paths, exit-code-only grep misses). Previously boilerplate causes and the model's own wrong guesses landed in `errors.md`. Tests: `tests/tool-failure-diagnosis.test.ts`.
20
+ - **Factual check resolves path suffixes** (`src/modules/hallucination/factual.ts`): `webui/state.ts` now matches `src/modules/webui/state.ts` via the cached basename index (no new FS walks), fixing false "mentioned files not found" warnings on short paths. Tests: `tests/hallucination-factual.test.ts`.
21
+ - **`edit_file` tolerates LF anchors in CRLF files** (`src/tools/edit-file.ts`): `spliceNormalized()` matches on normalized line endings and splices the original, keeping the file's ending style (no more "string not found" loops on Windows checkouts). Tests: `tests/edit-file.test.ts`.
22
+ - **Web usage re-render covers the context breakdown** (`src/modules/webui/assets/js/feed.js`): `h.contextBreakdown` added to the `renderUsage` signature so part shifts at constant used/limit repaint the segmented bar and legend. Guard: `tests/webui-assets.test.ts`.
23
+ - **Terminal context-budget part words are localized** (`src/cli/repl.ts`, `src/i18n/en.json`, `ru.json`): the startup banner showed untranslated latin `sys/res/hist` in both locales; now `repl.ctx_part_system/reserve/history` (`system/reserve/history` + `система/резерв/история`). Web legend (usage parts) intentionally unchanged. Tests: `tests/i18n.test.ts`.
24
+ - **Verification hygiene is model-facing** (`src/core/system-prompt.ts`, `src/tools/browser.ts`): verification `mma web` servers must run isolated (`MMA_CONFIG_DIR`), never against the user's live server; without vision capabilities the browser description steers to snapshot text and DOM/API assertions. Tests: `tests/core/bootstrap.test.ts`, `tests/tools/browser.test.ts`.
25
+
9
26
  ## [2.14.0] - 2026-10-01
10
27
 
11
28
  ### Changed
@@ -15,7 +15,7 @@ loadCommands,
15
15
  } from "./js/tabs.js";
16
16
  import {
17
17
  renderSelectors, renderSettings, loadConfig, loadModels, loadProviders,
18
- loadMemory, loadSkills, loadPlugins, applyTheme, currentTheme, applyFontSize, setConnStatus,
18
+ loadMemory, loadSkills, loadPlugins, applyTheme, currentTheme, applyFontSize, setConnStatus, setStaleBar,
19
19
  } from "./js/settings.js";
20
20
  import {
21
21
  initRail, setPlan as railPlan, railEvent, applyRailI18n,
@@ -88,6 +88,7 @@ function renderState(snapshot, busy) {
88
88
  sidebarSessionChanged(sessionId);
89
89
  renderWorkspace(S.state.host);
90
90
  renderSessionsDropdown();
91
+ setStaleBar(!!(snapshot && snapshot.i18nStale));
91
92
  }
92
93
 
93
94
  async function refresh() {
@@ -259,6 +260,7 @@ function applyStaticI18n() {
259
260
  applySidebarI18n();
260
261
  if (msgs.querySelector(".empty")) showEmptyFeed();
261
262
  if (connStatus && !connStatus.hidden) setConnStatus(true);
263
+ setStaleBar(undefined, true);
262
264
  }
263
265
 
264
266
  async function boot() {
@@ -29,6 +29,7 @@
29
29
  <button class="icon-btn" id="toggleSidebar" title="sidebar" aria-label="sidebar" aria-expanded="false">▤</button>
30
30
  <button class="icon-btn" id="openSettings" title="settings" aria-label="settings" aria-haspopup="dialog" aria-expanded="false">⚙</button>
31
31
  <span class="conn" id="connStatus" role="status" aria-live="polite" hidden></span>
32
+ <span class="conn stale" id="staleBar" role="status" hidden></span>
32
33
  </div>
33
34
 
34
35
  <div class="bar-sub" id="sessBar">
@@ -14,6 +14,7 @@ export const attachStrip = document.getElementById("attachStrip");
14
14
  export const attachErr = document.getElementById("attachErr");
15
15
  export const imgPicker = document.getElementById("imgPicker");
16
16
  export const connStatus = document.getElementById("connStatus");
17
+ export const staleBar = document.getElementById("staleBar");
17
18
  export const liveEl = document.getElementById("live");
18
19
  export const sendBtn = document.getElementById("sendBtn");
19
20
 
@@ -578,6 +578,30 @@ export function onToolEnd(data) {
578
578
  S.toolNodes.delete(data.toolCallId);
579
579
  const args = S.pendingArgs.get(data.toolCallId) || {};
580
580
  S.pendingArgs.delete(data.toolCallId);
581
+ // File tools render as a single filerow (viewer link + copy): the `.act`
582
+ // line from `onToolStart` is replaced, otherwise the feed shows the same
583
+ // action twice (act + filerow). Without a path `cardFor` renders nothing,
584
+ // so keep the generic act line in that case.
585
+ const filePath = args.path || args.file || args.dir;
586
+ if (FILE_TOOLS.indexOf(data.name) !== -1 && filePath) {
587
+ if (line && line.parentNode) line.remove();
588
+ if (verbosity() === "quiet") {
589
+ if (data.success) {
590
+ const card = cardFor(data.name, args, data.sessionId, data.success);
591
+ if (card) S.runNodes.push(card);
592
+ S.runDuration += typeof data.durationMs === "number" ? data.durationMs : 0;
593
+ } else {
594
+ const card = cardFor(data.name, args, data.sessionId, data.success);
595
+ const cause = firstOutputLine(data.output);
596
+ if (cause) insertAfter(card || line, el("div", "detail", "↳ " + cause));
597
+ if (card) S.runNodes.push(card);
598
+ }
599
+ return;
600
+ }
601
+ const card = cardFor(data.name, args, data.sessionId, data.success);
602
+ renderToolOutput(card || line, data.output);
603
+ return;
604
+ }
581
605
  if (verbosity() === "quiet") {
582
606
  // Quiet hides tool bodies. Track successful nodes so a run of read-only
583
607
  // actions can fold at the next segment; a failure keeps its line and adds
@@ -1015,6 +1039,58 @@ function usageRow(label, value, cls) {
1015
1039
  return row;
1016
1040
  }
1017
1041
 
1042
+ // Segmented context bar: system · tools · history · reserve · free, widths
1043
+ // relative to the full context window so the parts always sum to one bar.
1044
+ function contextSegBar(h, used, limit) {
1045
+ const bar = el("div", "bar segbar");
1046
+ const bd = h.contextBreakdown;
1047
+ if (!bd || typeof bd.window !== "number" || bd.window <= 0) {
1048
+ // Legacy fallback (pre-breakdown payloads): single used/limit fill.
1049
+ const fill = el("i");
1050
+ fill.style.width = Math.min(100, Math.round((used / limit) * 100)) + "%";
1051
+ bar.appendChild(fill);
1052
+ return bar;
1053
+ }
1054
+ const denom = Math.max(bd.window, bd.used + bd.reserve);
1055
+ const parts = [
1056
+ ["seg-system", bd.system],
1057
+ ["seg-tools", bd.tools],
1058
+ ["seg-history", bd.history],
1059
+ ["seg-reserve", bd.reserve],
1060
+ ["seg-free", bd.free],
1061
+ ];
1062
+ for (const [cls, n] of parts) {
1063
+ if (!n || n <= 0) continue;
1064
+ const seg = el("i", cls);
1065
+ seg.style.width = (n / denom) * 100 + "%";
1066
+ bar.appendChild(seg);
1067
+ }
1068
+ return bar;
1069
+ }
1070
+
1071
+ // Dim legend under the segmented bar: one dot+label+value per context part.
1072
+ function contextLegend(h) {
1073
+ const box = el("div", "seglegend");
1074
+ const bd = h.contextBreakdown;
1075
+ if (!bd) return box;
1076
+ const items = [
1077
+ ["seg-system", "web.usage.part_system", bd.system],
1078
+ ["seg-tools", "web.usage.part_tools", bd.tools],
1079
+ ["seg-history", "web.usage.part_history", bd.history],
1080
+ ["seg-reserve", "web.usage.part_reserve", bd.reserve],
1081
+ ["seg-free", "web.usage.part_free", bd.free],
1082
+ ];
1083
+ for (const [cls, labelKey, n] of items) {
1084
+ if (!n || n <= 0) continue;
1085
+ const item = el("span", "segl-item");
1086
+ item.appendChild(el("i", "segl-dot " + cls));
1087
+ item.appendChild(el("span", "segl-label", t(labelKey)));
1088
+ item.appendChild(el("b", "", fmtTokens(n)));
1089
+ box.appendChild(item);
1090
+ }
1091
+ return box;
1092
+ }
1093
+
1018
1094
  // Session row with a copy button next to the id (icon-only, ✓ on success).
1019
1095
  function usageSessionRow(sessionId) {
1020
1096
  const row = el("div", "kv");
@@ -1045,7 +1121,7 @@ export function renderUsage(h) {
1045
1121
  const cfg = S.state.config || {};
1046
1122
  const sig = JSON.stringify([
1047
1123
  h.sessionId, h.model, h.contextUsed, h.contextLimit, h.cacheHitRate, h.costUsd,
1048
- h.compactionCount, h.contextQuality, h.contextProbe, cfg.model, cfg.contextWindow,
1124
+ h.compactionCount, h.contextQuality, h.contextProbe, h.contextBreakdown, cfg.model, cfg.contextWindow,
1049
1125
  I18N.locale,
1050
1126
  ]);
1051
1127
  if (sig === S.lastUsageSig) return;
@@ -1058,11 +1134,8 @@ export function renderUsage(h) {
1058
1134
  const limit = typeof h.contextLimit === "number" ? h.contextLimit : cfg.contextWindow || null;
1059
1135
  usageEl.appendChild(usageRow(t("web.usage.context"), used != null && limit ? fmtTokens(used) + " / " + fmtTokens(limit) : "—"));
1060
1136
  if (used != null && limit) {
1061
- const bar = el("div", "bar");
1062
- const fill = el("i");
1063
- fill.style.width = Math.min(100, Math.round((used / limit) * 100)) + "%";
1064
- bar.appendChild(fill);
1065
- usageEl.appendChild(bar);
1137
+ usageEl.appendChild(contextSegBar(h, used, limit));
1138
+ usageEl.appendChild(contextLegend(h));
1066
1139
  }
1067
1140
  const hit = typeof h.cacheHitRate === "number" ? h.cacheHitRate : null;
1068
1141
  usageEl.appendChild(usageRow(t("web.usage.cache"), hit != null ? Math.round(hit * 100) + "%" : "—", hit != null ? "" : "dim"));
@@ -3,7 +3,7 @@
3
3
  // share from tabs; confirm/toast from dialog. Never imports app (the locale
4
4
  // refresh reaches the app shell through the store hooks registry).
5
5
  import {
6
- el, icon, link, inlineTextNodes, connStatus, makeToggle,
6
+ el, icon, link, inlineTextNodes, connStatus, staleBar, makeToggle,
7
7
  } from "./dom.js";
8
8
  import { api } from "./api.js";
9
9
  import { t, loadI18n } from "./i18n.js";
@@ -324,6 +324,26 @@ export function setConnStatus(disconnected) {
324
324
  connStatus.textContent = disconnected ? "⋯ " + t("web.conn.reconnecting") : "";
325
325
  }
326
326
 
327
+ // Stale-translations banner: dict files on disk are newer than the running
328
+ // server process, so the chrome may show raw keys. Rebuilt only on S.state
329
+ // change (not on every 2s poll) to keep the reload-button focus stable;
330
+ // `force` re-renders the label after a locale switch.
331
+ let lastStale = null;
332
+ export function setStaleBar(stale, force) {
333
+ if (!staleBar) return;
334
+ // undefined = перерисовать последнее состояние (смена локали).
335
+ const on = stale === undefined ? !!lastStale : !!stale;
336
+ if (!force && on === lastStale) return;
337
+ lastStale = on;
338
+ staleBar.hidden = !on;
339
+ staleBar.textContent = "";
340
+ if (!on) return;
341
+ staleBar.appendChild(el("span", "", "⚠ " + t("web.i18n.stale") + " "));
342
+ staleBar.appendChild(link(t("web.i18n.reload"), "", () => location.reload()));
343
+ }
344
+ return lastStale;
345
+ }
346
+
327
347
  // Panel → named sections lives in `settings-controls.js` (PANEL_SECTIONS).
328
348
  // Custom panels (model/ui/browser…) render a bespoke body first and then their
329
349
  // sections; purely generic panels (general/context/…) render sections only.
@@ -54,6 +54,12 @@
54
54
  --checker-border: #2e2e2e;
55
55
  --ph-light: #eaeef2;
56
56
  --track: #262626;
57
+ /* Context-breakdown segments (system/tools/history/reserve/free). */
58
+ --seg-system: #a78bfa;
59
+ --seg-tools: #38bdf8;
60
+ --seg-history: #34d399;
61
+ --seg-reserve: #fbbf24;
62
+ --seg-free: #3f3f46;
57
63
  --plan-line: #2e2e2e;
58
64
  --plan-bar-fg: #a3a3a3;
59
65
  --sec-h: #a3a3a3;
@@ -124,6 +130,12 @@ body.light {
124
130
  --checker-border: #d0d7de;
125
131
  --ph-light: #eaeef2;
126
132
  --track: #eaeef2;
133
+ /* Context-breakdown segments (system/tools/history/reserve/free). */
134
+ --seg-system: #7c3aed;
135
+ --seg-tools: #0284c7;
136
+ --seg-history: #059669;
137
+ --seg-reserve: #d97706;
138
+ --seg-free: #d0d7de;
127
139
  --plan-line: #d0d7de;
128
140
  --plan-bar-fg: #57606a;
129
141
  --sec-h: #57606a;
@@ -167,6 +179,8 @@ body[data-font="large"] { font-size: 17px; }
167
179
  #openSettings { margin-left: auto; margin-right: 6px; }
168
180
  #toggleSidebar { display: none; }
169
181
  .conn { color: var(--dim-a); font-size: 11.5px; white-space: nowrap; }
182
+ .conn.stale { color: var(--yellow); }
183
+ .conn.stale .linkbtn { font-size: 11.5px; }
170
184
  .tabstrip {
171
185
  display: flex; gap: 4px; align-items: center; overflow-x: auto;
172
186
  flex: 1; min-width: 0;
@@ -841,6 +855,26 @@ body.light .kv .wn { color: var(--yellow); }
841
855
  .bar { height: 4px; background: var(--track); border-radius: 2px; margin-top: 6px; overflow: hidden; }
842
856
  .bar i { display: block; height: 100%; width: 0; background: var(--green-fill); transition: width .2s; }
843
857
 
858
+ /* Segmented context bar: flex row so widths add up to the full window. */
859
+ .bar.segbar { display: flex; }
860
+ .bar.segbar i { width: auto; flex: 0 0 auto; transition: width .25s; }
861
+ .bar.segbar .seg-system { background: var(--seg-system); }
862
+ .bar.segbar .seg-tools { background: var(--seg-tools); }
863
+ .bar.segbar .seg-history { background: var(--seg-history); }
864
+ .bar.segbar .seg-reserve { background: var(--seg-reserve); }
865
+ .bar.segbar .seg-free { background: var(--seg-free); }
866
+ /* Legend of context parts (dim dot + label + tokens): rows, no boxes. */
867
+ .seglegend { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 5px; }
868
+ .segl-item { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--dim-b); }
869
+ .segl-dot { width: 7px; height: 7px; border-radius: 2px; flex: 0 0 auto; }
870
+ .segl-dot.seg-system { background: var(--seg-system); }
871
+ .segl-dot.seg-tools { background: var(--seg-tools); }
872
+ .segl-dot.seg-history { background: var(--seg-history); }
873
+ .segl-dot.seg-reserve { background: var(--seg-reserve); }
874
+ .segl-dot.seg-free { background: var(--seg-free); }
875
+ .segl-item b { color: var(--muted); font-weight: 600; }
876
+ .segl-label { color: var(--dim-b); }
877
+
844
878
  /* ---- settings modal + nested dialogs (provider, confirm) ---- */
845
879
  .overlay {
846
880
  display: none; position: fixed; inset: 0; z-index: 50;
package/dist/i18n/en.json CHANGED
@@ -592,6 +592,9 @@
592
592
  "repl.model": "Model:",
593
593
  "repl.provider": "Provider:",
594
594
  "repl.context": "Context:",
595
+ "repl.ctx_part_system": "system",
596
+ "repl.ctx_part_reserve": "reserve",
597
+ "repl.ctx_part_history": "history",
595
598
  "repl.sysprompt_label": "System prompt:",
596
599
  "repl.sysprompt_size": "{used} / {budget} tokens",
597
600
  "repl.sysprompt_desc": "System prompt size and blocks (use --full for the text)",
@@ -1052,6 +1055,8 @@
1052
1055
  "web.sessbar.prev": "previous session",
1053
1056
  "web.sessbar.next": "next session",
1054
1057
  "web.conn.reconnecting": "reconnecting…",
1058
+ "web.i18n.stale": "translations updated — restart the server",
1059
+ "web.i18n.reload": "reload page",
1055
1060
  "web.sidebar.plan": "plan",
1056
1061
  "web.sidebar.session": "session",
1057
1062
  "web.sidebar.usage": "usage",
@@ -1118,6 +1123,11 @@
1118
1123
  "web.usage.cache": "cache",
1119
1124
  "web.usage.cost": "cost",
1120
1125
  "web.usage.compactions": "compactions",
1126
+ "web.usage.part_system": "instructions",
1127
+ "web.usage.part_tools": "tools",
1128
+ "web.usage.part_history": "dialogue",
1129
+ "web.usage.part_reserve": "response reserve",
1130
+ "web.usage.part_free": "free",
1121
1131
  "web.usage.quality": "quality",
1122
1132
  "web.usage.probe": "probe",
1123
1133
  "web.usage.probe_loaded": "{actual} loaded",
package/dist/i18n/ru.json CHANGED
@@ -589,6 +589,9 @@
589
589
  "repl.model": "Модель:",
590
590
  "repl.provider": "Провайдер:",
591
591
  "repl.context": "Контекст:",
592
+ "repl.ctx_part_system": "система",
593
+ "repl.ctx_part_reserve": "резерв",
594
+ "repl.ctx_part_history": "история",
592
595
  "repl.sysprompt_label": "Системный промпт:",
593
596
  "repl.sysprompt_size": "{used} / {budget} токенов",
594
597
  "repl.sysprompt_desc": "Размер системного промпта и блоки (текст — через --full)",
@@ -1055,6 +1058,8 @@
1055
1058
  "web.sessbar.prev": "предыдущая сессия",
1056
1059
  "web.sessbar.next": "следующая сессия",
1057
1060
  "web.conn.reconnecting": "переподключение…",
1061
+ "web.i18n.stale": "переводы обновлены — перезапустите сервер",
1062
+ "web.i18n.reload": "обновить страницу",
1058
1063
  "web.sidebar.plan": "план",
1059
1064
  "web.sidebar.session": "сессия",
1060
1065
  "web.sidebar.usage": "использование",
@@ -1121,6 +1126,11 @@
1121
1126
  "web.usage.cache": "кеш",
1122
1127
  "web.usage.cost": "стоимость",
1123
1128
  "web.usage.compactions": "сжатия",
1129
+ "web.usage.part_system": "инструкции",
1130
+ "web.usage.part_tools": "инструменты",
1131
+ "web.usage.part_history": "диалог",
1132
+ "web.usage.part_reserve": "резерв ответа",
1133
+ "web.usage.part_free": "свободно",
1124
1134
  "web.usage.quality": "качество",
1125
1135
  "web.usage.probe": "проб",
1126
1136
  "web.usage.probe_loaded": "загружено {actual}",
package/dist/main.js CHANGED
@@ -3073,6 +3073,9 @@ Available commands:`,
3073
3073
  "repl.model": "Model:",
3074
3074
  "repl.provider": "Provider:",
3075
3075
  "repl.context": "Context:",
3076
+ "repl.ctx_part_system": "system",
3077
+ "repl.ctx_part_reserve": "reserve",
3078
+ "repl.ctx_part_history": "history",
3076
3079
  "repl.sysprompt_label": "System prompt:",
3077
3080
  "repl.sysprompt_size": "{used} / {budget} tokens",
3078
3081
  "repl.sysprompt_desc": "System prompt size and blocks (use --full for the text)",
@@ -3577,6 +3580,8 @@ Updated: {updatedAt}`,
3577
3580
  "web.sessbar.prev": "previous session",
3578
3581
  "web.sessbar.next": "next session",
3579
3582
  "web.conn.reconnecting": "reconnecting…",
3583
+ "web.i18n.stale": "translations updated — restart the server",
3584
+ "web.i18n.reload": "reload page",
3580
3585
  "web.sidebar.plan": "plan",
3581
3586
  "web.sidebar.session": "session",
3582
3587
  "web.sidebar.usage": "usage",
@@ -3643,6 +3648,11 @@ Updated: {updatedAt}`,
3643
3648
  "web.usage.cache": "cache",
3644
3649
  "web.usage.cost": "cost",
3645
3650
  "web.usage.compactions": "compactions",
3651
+ "web.usage.part_system": "instructions",
3652
+ "web.usage.part_tools": "tools",
3653
+ "web.usage.part_history": "dialogue",
3654
+ "web.usage.part_reserve": "response reserve",
3655
+ "web.usage.part_free": "free",
3646
3656
  "web.usage.quality": "quality",
3647
3657
  "web.usage.probe": "probe",
3648
3658
  "web.usage.probe_loaded": "{actual} loaded",
@@ -4972,6 +4982,9 @@ var init_ru = __esm(() => {
4972
4982
  "repl.model": "Модель:",
4973
4983
  "repl.provider": "Провайдер:",
4974
4984
  "repl.context": "Контекст:",
4985
+ "repl.ctx_part_system": "система",
4986
+ "repl.ctx_part_reserve": "резерв",
4987
+ "repl.ctx_part_history": "история",
4975
4988
  "repl.sysprompt_label": "Системный промпт:",
4976
4989
  "repl.sysprompt_size": "{used} / {budget} токенов",
4977
4990
  "repl.sysprompt_desc": "Размер системного промпта и блоки (текст — через --full)",
@@ -5483,6 +5496,8 @@ var init_ru = __esm(() => {
5483
5496
  "web.sessbar.prev": "предыдущая сессия",
5484
5497
  "web.sessbar.next": "следующая сессия",
5485
5498
  "web.conn.reconnecting": "переподключение…",
5499
+ "web.i18n.stale": "переводы обновлены — перезапустите сервер",
5500
+ "web.i18n.reload": "обновить страницу",
5486
5501
  "web.sidebar.plan": "план",
5487
5502
  "web.sidebar.session": "сессия",
5488
5503
  "web.sidebar.usage": "использование",
@@ -5549,6 +5564,11 @@ var init_ru = __esm(() => {
5549
5564
  "web.usage.cache": "кеш",
5550
5565
  "web.usage.cost": "стоимость",
5551
5566
  "web.usage.compactions": "сжатия",
5567
+ "web.usage.part_system": "инструкции",
5568
+ "web.usage.part_tools": "инструменты",
5569
+ "web.usage.part_history": "диалог",
5570
+ "web.usage.part_reserve": "резерв ответа",
5571
+ "web.usage.part_free": "свободно",
5552
5572
  "web.usage.quality": "качество",
5553
5573
  "web.usage.probe": "проб",
5554
5574
  "web.usage.probe_loaded": "загружено {actual}",
@@ -11093,7 +11113,7 @@ function buildSystemInfo(config, baseDir, profileCompressed) {
11093
11113
  if (config.autoPlan) {
11094
11114
  lines.push(`PLAN (MANDATORY): file creation, package installs, or multi-action tasks MUST create a "plan" BEFORE starting.`);
11095
11115
  }
11096
- lines.push(`Runtime verification: when a bug is described in terms of runtime behavior`, `(what happens when the app runs), reproduce it —`, `run the app / build / browser tool — confirm the fix against actual runtime output`, `before calling "plan_step step=N status=done".`, `Check existing servers first (process_list/process_log): a stale dev server`, `runs the old pipeline and keeps failing after the fix.`, `Restart/kill it (process_kill) and verify the exact endpoint/port reported —`, `a fresh port does NOT prove the user's error is gone.`);
11116
+ lines.push(`Runtime verification: when a bug is described in terms of runtime behavior`, `(what happens when the app runs), reproduce it —`, `run the app / build / browser tool — confirm the fix against actual runtime output`, `before calling "plan_step step=N status=done".`, `Check existing servers first (process_list/process_log): a stale dev server`, `runs the old pipeline and keeps failing after the fix.`, `Restart/kill it (process_kill) and verify the exact endpoint/port reported —`, `a fresh port does NOT prove the user's error is gone.`, `Verification "mma web" servers run isolated (MMA_CONFIG_DIR=<empty tmp dir>) —`, `never verify against the user's live server/sessions/state.`);
11097
11117
  const sec = config.security;
11098
11118
  if (sec?.enabled) {
11099
11119
  const secLines = [
@@ -14404,6 +14424,40 @@ var init_write_file = __esm(() => {
14404
14424
  // src/tools/edit-file.ts
14405
14425
  import { readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
14406
14426
  import { resolve as resolve6 } from "path";
14427
+ function spliceNormalized(content, oldStr, newStr) {
14428
+ const normOld = oldStr.replace(/\r\n/g, `
14429
+ `);
14430
+ const norm = [];
14431
+ const map = [];
14432
+ for (let i = 0;i < content.length; i++) {
14433
+ if (content[i] === "\r" && content[i + 1] === `
14434
+ `) {
14435
+ norm.push(`
14436
+ `);
14437
+ map.push(i);
14438
+ i++;
14439
+ } else {
14440
+ norm.push(content[i]);
14441
+ map.push(i);
14442
+ }
14443
+ }
14444
+ const normContent = norm.join("");
14445
+ const idx = normContent.indexOf(normOld);
14446
+ if (idx === -1 || normOld.length === 0)
14447
+ return null;
14448
+ const startOrig = map[idx];
14449
+ const endNorm = idx + normOld.length;
14450
+ const endOrig = endNorm < map.length ? map[endNorm] : content.length;
14451
+ let insert = newStr;
14452
+ if (content.includes(`\r
14453
+ `) && !newStr.includes(`\r
14454
+ `) && newStr.includes(`
14455
+ `)) {
14456
+ insert = newStr.replace(/\n/g, `\r
14457
+ `);
14458
+ }
14459
+ return content.slice(0, startOrig) + insert + content.slice(endOrig);
14460
+ }
14407
14461
  var editFileTool;
14408
14462
  var init_edit_file = __esm(() => {
14409
14463
  init_i18n();
@@ -14488,13 +14542,19 @@ var init_edit_file = __esm(() => {
14488
14542
  output: t("file.read_failed", { path, error: errMsg(err) })
14489
14543
  };
14490
14544
  }
14491
- if (!content.includes(oldStr)) {
14492
- return {
14493
- success: false,
14494
- output: t("file.string_not_found", { str: oldStr, path })
14495
- };
14545
+ let updated;
14546
+ if (content.includes(oldStr)) {
14547
+ updated = content.replace(oldStr, () => newStr);
14548
+ } else {
14549
+ const spliced = spliceNormalized(content, oldStr, newStr);
14550
+ if (!spliced) {
14551
+ return {
14552
+ success: false,
14553
+ output: t("file.string_not_found", { str: oldStr, path })
14554
+ };
14555
+ }
14556
+ updated = spliced;
14496
14557
  }
14497
- const updated = content.replace(oldStr, () => newStr);
14498
14558
  if (securityConfig?.enabled && securityConfig?.contentScan?.enabled) {
14499
14559
  const scanResult = scanContent(updated, path, securityConfig.contentScan);
14500
14560
  if (!scanResult.allowed) {
@@ -21460,6 +21520,27 @@ function queueToolImages(sink, images) {
21460
21520
  }
21461
21521
 
21462
21522
  // src/core/agent/tool-batch.ts
21523
+ function diagnoseRepeatedFailures(tool, headlines) {
21524
+ const genericCause = "Repeated failures suggest a systemic problem (config, environment, or a broken tool), not a one-off";
21525
+ const genericSolution = "Check the error message, verify the tool's dependencies are installed/configured, and consider a different tool";
21526
+ const modelMiss = tool === "bash" ? [/did not match any test files?/i, /no such file/i, /cannot find (the )?path/i] : [];
21527
+ if (headlines.length > 0 && headlines.every((h) => /^\(exit code \d+\)$/.test(h.trim()) || modelMiss.some((re) => re.test(h)))) {
21528
+ return {
21529
+ skip: true,
21530
+ cause: "Model named a wrong target (path/filter); the tool itself works",
21531
+ solution: "Re-check the target path or filter instead of blaming the tool"
21532
+ };
21533
+ }
21534
+ if (tool === "edit_file" && headlines.some((h) => /string not found|не найдена/i.test(h))) {
21535
+ return {
21536
+ skip: false,
21537
+ cause: "Anchor mismatch between the model anchor and file content — often CRLF vs LF line endings or a stale read, not a broken tool",
21538
+ solution: "Re-read the exact block with read_file and retry with a short unique single-line anchor"
21539
+ };
21540
+ }
21541
+ return { skip: false, cause: genericCause, solution: genericSolution };
21542
+ }
21543
+
21463
21544
  class ToolBatchExecutor {
21464
21545
  deps;
21465
21546
  constructor(deps) {
@@ -21619,15 +21700,22 @@ ${result.diff}
21619
21700
  this.deps.compactionService.compactAfterTool(state);
21620
21701
  if (!result.success && !result.blocked && !infoTools.has(call.name)) {
21621
21702
  const key = call.name;
21622
- const prev = state.toolFailureCounts.get(key) ?? { count: 0, error: "" };
21703
+ const prev = state.toolFailureCounts.get(key) ?? { count: 0, error: "", samples: [] };
21623
21704
  prev.count++;
21624
- prev.error = String(result.output ?? "").slice(0, 200);
21705
+ const errText = String(result.output ?? "");
21706
+ prev.error = errText.slice(0, 200);
21707
+ const headline = errText.split(`
21708
+ `)[0].slice(0, 160).trim();
21709
+ if (headline && !prev.samples.includes(headline) && prev.samples.length < 3) {
21710
+ prev.samples.push(headline);
21711
+ }
21625
21712
  state.toolFailureCounts.set(key, prev);
21626
21713
  if (prev.count >= MIN_REPEATED_TOOL_FAILURES && !state.memoryRuleRecorded.has(key)) {
21627
21714
  state.memoryRuleRecorded.add(key);
21628
21715
  const memStore = this.deps.memoryStore;
21629
- if (memStore) {
21630
- memStore.appendRule("errors", `Tool ${key} failed ${prev.count}x (${prev.error})`, `Repeated ${key} failures suggest a systemic problem (config, environment, or a broken tool), not a one-off`, "Check the error message, verify the tool's dependencies are installed/configured, and consider a different tool");
21716
+ const diagnosis = diagnoseRepeatedFailures(key, prev.samples);
21717
+ if (memStore && !diagnosis.skip) {
21718
+ memStore.appendRule("errors", `Tool ${key} failed ${prev.count}x (${prev.samples.join(" | ") || prev.error})`, diagnosis.cause, diagnosis.solution);
21631
21719
  logger3.diagnostic("warn", `Recorded repeated ${key} failures to memory (${prev.count}x)`);
21632
21720
  }
21633
21721
  }
@@ -22459,6 +22547,7 @@ var init_provider = __esm(() => {
22459
22547
 
22460
22548
  // src/modules/critic/evidence.ts
22461
22549
  import { readFileSync as readFileSync26 } from "fs";
22550
+ import { execFileSync } from "node:child_process";
22462
22551
  import { resolve as resolve15 } from "path";
22463
22552
  function isWorkMutation(name) {
22464
22553
  return MUTATING_TOOLS.has(name) || name === "bash";
@@ -22476,6 +22565,10 @@ function buildEvidencePack(input) {
22476
22565
  if (cmd)
22477
22566
  commands.push(cmd);
22478
22567
  }
22568
+ if (input.baseDir) {
22569
+ for (const f of gitChangedFiles(input.baseDir))
22570
+ changedFiles.add(f);
22571
+ }
22479
22572
  return {
22480
22573
  task: input.task,
22481
22574
  planSummary: input.planSummary,
@@ -22500,6 +22593,45 @@ function changedFileOf(ev) {
22500
22593
  return;
22501
22594
  return typeof ev.args?.path === "string" ? ev.args.path : undefined;
22502
22595
  }
22596
+ function parsePorcelainLine(line) {
22597
+ if (line.length < 4)
22598
+ return null;
22599
+ let p = line.slice(3).trim();
22600
+ const arrow = p.indexOf(" -> ");
22601
+ if (arrow !== -1)
22602
+ p = p.slice(arrow + 4).trim();
22603
+ if (p.length >= 2 && p.startsWith('"') && p.endsWith('"')) {
22604
+ try {
22605
+ p = JSON.parse(p);
22606
+ } catch {
22607
+ p = p.slice(1, -1);
22608
+ }
22609
+ }
22610
+ return p || null;
22611
+ }
22612
+ function gitChangedFiles(baseDir) {
22613
+ let out;
22614
+ try {
22615
+ out = execFileSync("git", ["status", "--porcelain"], {
22616
+ cwd: resolve15(baseDir),
22617
+ encoding: "utf-8",
22618
+ stdio: ["ignore", "pipe", "ignore"],
22619
+ timeout: 1e4
22620
+ });
22621
+ } catch {
22622
+ return [];
22623
+ }
22624
+ const files = [];
22625
+ for (const line of out.split(`
22626
+ `)) {
22627
+ const p = parsePorcelainLine(line);
22628
+ if (p)
22629
+ files.push(p);
22630
+ if (files.length >= MAX_GIT_FILES)
22631
+ break;
22632
+ }
22633
+ return files;
22634
+ }
22503
22635
  function commandOf(ev) {
22504
22636
  if (ev.tool !== "bash" || typeof ev.args?.command !== "string")
22505
22637
  return;
@@ -22523,7 +22655,7 @@ function readExcerpts(baseDir, paths) {
22523
22655
  }
22524
22656
  return out;
22525
22657
  }
22526
- var MUTATING_TOOLS, MAX_DIGEST_LINES = 40, MAX_DIGEST_OUTPUT_CHARS = 300, MAX_EXCERPT_FILES = 5, MAX_EXCERPT_CHARS = 1200, MAX_ANSWER_CHARS = 2000;
22658
+ var MUTATING_TOOLS, MAX_DIGEST_LINES = 40, MAX_DIGEST_OUTPUT_CHARS = 300, MAX_EXCERPT_FILES = 5, MAX_EXCERPT_CHARS = 1200, MAX_ANSWER_CHARS = 2000, MAX_GIT_FILES = 20;
22527
22659
  var init_evidence = __esm(() => {
22528
22660
  init_tool_output();
22529
22661
  init_context_log();
@@ -23660,7 +23792,8 @@ class Agent {
23660
23792
  costBreakdown: this.costTracker.breakdown(),
23661
23793
  cache: cacheStats,
23662
23794
  compactionCount: contextManager.getCompactionCount(),
23663
- contextQuality: contextManager.getQuality()
23795
+ contextQuality: contextManager.getQuality(),
23796
+ contextBreakdown: contextManager.getBreakdown?.()
23664
23797
  };
23665
23798
  }
23666
23799
  return {
@@ -23678,6 +23811,7 @@ class Agent {
23678
23811
  cache: cacheStats,
23679
23812
  compactionCount: contextManager.getCompactionCount(),
23680
23813
  contextQuality: contextManager.getQuality(),
23814
+ contextBreakdown: contextManager.getBreakdown?.(),
23681
23815
  llmDurationMs: state.totalLlmDuration
23682
23816
  };
23683
23817
  }
@@ -24290,6 +24424,28 @@ ${lines.join(`
24290
24424
  summary: this.compactedBlock ?? ""
24291
24425
  };
24292
24426
  }
24427
+ systemTokens() {
24428
+ const sys = this.messages.find((m) => m.role === "system");
24429
+ return sys ? this.estimateMessageTokensCached(sys) : 0;
24430
+ }
24431
+ getBreakdown() {
24432
+ const used = this.getEstimatedTokens();
24433
+ const system = this.systemTokens();
24434
+ const tools = this.toolTokens;
24435
+ const history = Math.max(0, used - system - tools);
24436
+ const b = this.budget;
24437
+ return {
24438
+ system,
24439
+ tools,
24440
+ history,
24441
+ used,
24442
+ systemBudget: b.systemPrompt,
24443
+ historyBudget: b.history,
24444
+ reserve: b.responseReserve,
24445
+ free: Math.max(0, this.contextWindow - used - b.responseReserve),
24446
+ window: this.contextWindow
24447
+ };
24448
+ }
24293
24449
  getSnapshot() {
24294
24450
  return {
24295
24451
  window: this.contextWindow,
@@ -24299,7 +24455,8 @@ ${lines.join(`
24299
24455
  messageCount: this.messages.length,
24300
24456
  quality: this.getQuality(),
24301
24457
  compactionCount: this.compactionCount,
24302
- iterationsSinceCompaction: this.iterationsSinceCompaction
24458
+ iterationsSinceCompaction: this.iterationsSinceCompaction,
24459
+ breakdown: this.getBreakdown()
24303
24460
  };
24304
24461
  }
24305
24462
  updateSystemPrompt(content) {
@@ -24504,7 +24661,33 @@ var init_factual = __esm(() => {
24504
24661
  if (!fp.includes("/") && !fp.includes("\\")) {
24505
24662
  return this.bareNameExists(fp);
24506
24663
  }
24507
- return false;
24664
+ return this.suffixExists(fp);
24665
+ }
24666
+ suffixExists(fp) {
24667
+ const norm = fp.replace(/\\/g, "/");
24668
+ const tail = `/${norm}`;
24669
+ const base = norm.split("/").pop() ?? norm;
24670
+ const fwd = (p) => p.replace(/\\/g, "/");
24671
+ for (const known of this.knownFiles) {
24672
+ if (fwd(known).endsWith(tail))
24673
+ return true;
24674
+ }
24675
+ const checkIndex = () => {
24676
+ for (const full of this.getNameIndex().get(base) ?? []) {
24677
+ if (fwd(full).endsWith(tail))
24678
+ return true;
24679
+ }
24680
+ return false;
24681
+ };
24682
+ if (checkIndex())
24683
+ return true;
24684
+ const now = Date.now();
24685
+ if (now - this.lastRescan < FactualCheck.RESCAN_COOLDOWN_MS)
24686
+ return false;
24687
+ this.lastRescan = now;
24688
+ this.nameCache = null;
24689
+ this.nameCacheTime = 0;
24690
+ return checkIndex();
24508
24691
  }
24509
24692
  dotfileVariants(fp) {
24510
24693
  const variants = [fp];
@@ -28751,6 +28934,7 @@ function createBrowserTool() {
28751
28934
  "Do NOT use it for search queries: search engines (Google, Yandex, Bing) block headless browsers with CAPTCHA/429/redirect pages — use the web_search tool for searching.",
28752
28935
  "Each snapshot returns: a numbered list of interactive elements, the visible page text (Content section), browser console messages, and network errors.",
28753
28936
  "Use the Content section to understand what the page says; use element numbers as targets for click/type.",
28937
+ "Without vision capabilities screenshots return only a text note — verify visuals through snapshot text, and for local web UI prefer DOM/API assertions (fetch /api/* endpoints, grep built assets) over driving the browser.",
28754
28938
  "Actions: open (url), click (target), type (target, text), scroll (direction), back, forward, screenshot, snapshot, close, wait (ms)."
28755
28939
  ].join(" "),
28756
28940
  parameters: {
@@ -32393,11 +32577,26 @@ var init_plan_tool = __esm(() => {
32393
32577
  });
32394
32578
 
32395
32579
  // src/modules/execution/module.ts
32396
- import { existsSync as existsSync51, readFileSync as readFileSync37 } from "fs";
32580
+ import { existsSync as existsSync51, readFileSync as readFileSync37, statSync as statSync11 } from "fs";
32397
32581
  import { resolve as resolve22 } from "path";
32398
32582
  function planPathToken(p) {
32399
32583
  return toForwardSlash(p).toLowerCase();
32400
32584
  }
32585
+ function provablyStale(resolved, sinceMs) {
32586
+ if (Number.isNaN(sinceMs))
32587
+ return false;
32588
+ for (const r of resolved) {
32589
+ if (!r)
32590
+ return false;
32591
+ try {
32592
+ if (statSync11(r).mtimeMs > sinceMs)
32593
+ return false;
32594
+ } catch {
32595
+ return false;
32596
+ }
32597
+ }
32598
+ return true;
32599
+ }
32401
32600
 
32402
32601
  class ExecutionModule {
32403
32602
  name = "execution";
@@ -32641,6 +32840,8 @@ class ExecutionModule {
32641
32840
  });
32642
32841
  if (empty)
32643
32842
  continue;
32843
+ if (provablyStale(resolved, Date.parse(plan.createdAt)))
32844
+ continue;
32644
32845
  }
32645
32846
  step.status = "done";
32646
32847
  step.note = `auto-closed by final audit: ${step.kind === "delete" ? "all named files removed" : "all named files exist with content"} (${tokens.join(", ")})`;
@@ -32868,6 +33069,11 @@ class ExecutionModule {
32868
33069
  const satisfied = step.kind === "delete" ? allGone && !allExist : allExist;
32869
33070
  if (!satisfied)
32870
33071
  return;
33072
+ if (step.kind !== "delete") {
33073
+ const plan = this.tracker?.getPlan();
33074
+ if (plan && provablyStale(resolved, Date.parse(plan.createdAt)))
33075
+ return;
33076
+ }
32871
33077
  if (step.kind !== "delete") {
32872
33078
  const emptyFiles = [];
32873
33079
  for (const r of resolved) {
@@ -44567,7 +44773,7 @@ var init_loader3 = __esm(() => {
44567
44773
  });
44568
44774
 
44569
44775
  // src/modules/certification/fact-checker.ts
44570
- import { existsSync as existsSync56, readFileSync as readFileSync40, statSync as statSync11 } from "fs";
44776
+ import { existsSync as existsSync56, readFileSync as readFileSync40, statSync as statSync12 } from "fs";
44571
44777
  import { join as join50 } from "path";
44572
44778
  function checkSandbox(sandboxDir, checks, exitCode, output) {
44573
44779
  const failures = [];
@@ -44613,14 +44819,14 @@ function runCheck2(sandboxDir, check, exitCode, output) {
44613
44819
  }
44614
44820
  function isFile(p) {
44615
44821
  try {
44616
- return existsSync56(p) && statSync11(p).isFile();
44822
+ return existsSync56(p) && statSync12(p).isFile();
44617
44823
  } catch {
44618
44824
  return false;
44619
44825
  }
44620
44826
  }
44621
44827
  function isDir(p) {
44622
44828
  try {
44623
- return existsSync56(p) && statSync11(p).isDirectory();
44829
+ return existsSync56(p) && statSync12(p).isDirectory();
44624
44830
  } catch {
44625
44831
  return false;
44626
44832
  }
@@ -49242,7 +49448,7 @@ import { homedir as homedir8 } from "os";
49242
49448
  // src/cli/completer.ts
49243
49449
  init_registry2();
49244
49450
  init_i18n();
49245
- import { readdirSync as readdirSync19, statSync as statSync12 } from "fs";
49451
+ import { readdirSync as readdirSync19, statSync as statSync13 } from "fs";
49246
49452
  import { join as join54 } from "path";
49247
49453
  function buildSlashCompletions(commands) {
49248
49454
  return Array.from(commands.entries()).map(([name, cmd]) => ({
@@ -49316,7 +49522,7 @@ class FilePathProvider {
49316
49522
  const matches = [];
49317
49523
  for (const entry of entries) {
49318
49524
  const entryPath = join54(fullPath, entry);
49319
- const stat2 = statSync12(entryPath);
49525
+ const stat2 = statSync13(entryPath);
49320
49526
  const completion = prefix + entry + (stat2.isDirectory() ? "/" : "");
49321
49527
  if (completion.startsWith(ctx.partial)) {
49322
49528
  matches.push(completion);
@@ -51320,7 +51526,7 @@ ${divider()}`);
51320
51526
  row(t("repl.model"), pc.white(this.config.model));
51321
51527
  const endpoint = activeProviderEndpoint(this.config.provider);
51322
51528
  row(t("repl.provider"), `${endpoint.type} → ${pc.dim(endpoint.baseUrl)}`);
51323
- row(t("repl.context"), `${pc.white(String(ctx))} ${pc.dim(`(sys:${sysBudget} res:${resBudget} hist:${histBudget})`)}`);
51529
+ row(t("repl.context"), `${pc.white(String(ctx))} ${pc.dim(`(${t("repl.ctx_part_system")}:${sysBudget} ${t("repl.ctx_part_reserve")}:${resBudget} ${t("repl.ctx_part_history")}:${histBudget})`)}`);
51324
51530
  const si = this.agent.getSystemPromptInfo();
51325
51531
  row(t("repl.sysprompt_label"), pc.dim(t("repl.sysprompt_size", { used: si.tokenCount, budget: sysBudget })));
51326
51532
  if (this.skillsModule) {
@@ -51544,7 +51750,7 @@ init_output();
51544
51750
  init_i18n();
51545
51751
  init_colors();
51546
51752
  import { existsSync as existsSync63 } from "node:fs";
51547
- import { join as join60 } from "node:path";
51753
+ import { join as join61 } from "node:path";
51548
51754
 
51549
51755
  // src/modules/webui/module.ts
51550
51756
  init_config2();
@@ -52760,10 +52966,28 @@ function handleModelsGet(res, s) {
52760
52966
 
52761
52967
  // src/modules/webui/i18n-api.ts
52762
52968
  init_i18n();
52969
+
52970
+ // src/modules/webui/i18n-stale.ts
52971
+ import { statSync as statSync14 } from "fs";
52972
+ import { join as join60 } from "path";
52973
+ var serverStartedAt = Date.now();
52974
+ var dictFiles = ["en.json", "ru.json"].map((f) => join60(import.meta.dirname, "..", "..", "i18n", f));
52975
+ function isI18nStale() {
52976
+ for (const p of dictFiles) {
52977
+ try {
52978
+ if (statSync14(p).mtimeMs > serverStartedAt)
52979
+ return true;
52980
+ } catch {}
52981
+ }
52982
+ return false;
52983
+ }
52984
+
52985
+ // src/modules/webui/i18n-api.ts
52763
52986
  function handleI18nGet(res, s) {
52764
52987
  sendJson(res, 200, {
52765
52988
  locale: s.config?.locale ?? "en",
52766
- dicts: getFlatDicts()
52989
+ dicts: getFlatDicts(),
52990
+ i18nStale: isI18nStale()
52767
52991
  });
52768
52992
  }
52769
52993
 
@@ -54584,6 +54808,8 @@ function captureUsage(result, fallbackLimit) {
54584
54808
  }
54585
54809
  if (typeof result.contextQuality === "number")
54586
54810
  usage.contextQuality = result.contextQuality;
54811
+ if (result.contextBreakdown)
54812
+ usage.contextBreakdown = result.contextBreakdown;
54587
54813
  return usage;
54588
54814
  }
54589
54815
  function planEventFor(toolName, execModule) {
@@ -54606,10 +54832,12 @@ function buildHostState(deps, busy) {
54606
54832
  compactionCount: deps.usage?.compactionCount,
54607
54833
  cacheHitRate: deps.usage?.cacheHitRate,
54608
54834
  contextQuality: deps.usage?.contextQuality,
54835
+ contextBreakdown: deps.usage?.contextBreakdown,
54609
54836
  contextProbe: deps.contextProbe,
54610
54837
  workspace: deps.workspace,
54611
54838
  instructions: deps.instructions,
54612
54839
  projectMap: deps.projectMap ?? undefined,
54840
+ i18nStale: isI18nStale(),
54613
54841
  plan: planSnapshotFromModule(adaptActivePlan(deps.execModule?.getActivePlan()))
54614
54842
  };
54615
54843
  }
@@ -54811,7 +55039,7 @@ async function runWeb(opts) {
54811
55039
  process.env[MMA_ENV.WEBUI_OPEN] = "0";
54812
55040
  const mmaDir = globalMmaDir();
54813
55041
  const { hasDomainFiles: hasDomainFiles2 } = await Promise.resolve().then(() => (init_domains(), exports_domains));
54814
- const virgin = !existsSync63(join60(mmaDir, "config.json")) && !hasDomainFiles2(mmaDir);
55042
+ const virgin = !existsSync63(join61(mmaDir, "config.json")) && !hasDomainFiles2(mmaDir);
54815
55043
  const {
54816
55044
  agent,
54817
55045
  config,
@@ -54933,7 +55161,7 @@ init_config2();
54933
55161
  init_i18n();
54934
55162
  init_colors();
54935
55163
  import { existsSync as existsSync64, readFileSync as readFileSync45 } from "fs";
54936
- import { join as join62, dirname as dirname30 } from "path";
55164
+ import { join as join63, dirname as dirname30 } from "path";
54937
55165
  import { fileURLToPath as fileURLToPath7 } from "url";
54938
55166
 
54939
55167
  // src/modules/updater/index.ts
@@ -55055,7 +55283,7 @@ init_i18n();
55055
55283
  init_utils();
55056
55284
  init_paths();
55057
55285
  import { appendFileSync as appendFileSync8, mkdirSync as mkdirSync30 } from "fs";
55058
- import { join as join61 } from "path";
55286
+ import { join as join62 } from "path";
55059
55287
  import { homedir as homedir9 } from "os";
55060
55288
  var CRASH_LOG_DIR = globalLogsDir();
55061
55289
  var CRASH_LOG_FILE = "crash.jsonl";
@@ -55073,7 +55301,7 @@ function formatCrashEntry(type2, err) {
55073
55301
  function writeCrashEntry(dir, entry) {
55074
55302
  try {
55075
55303
  mkdirSync30(dir, { recursive: true });
55076
- appendFileSync8(join61(dir, CRASH_LOG_FILE), `${JSON.stringify(entry)}
55304
+ appendFileSync8(join62(dir, CRASH_LOG_FILE), `${JSON.stringify(entry)}
55077
55305
  `, "utf-8");
55078
55306
  } catch {}
55079
55307
  }
@@ -55144,7 +55372,7 @@ function resolveEarlyLocale() {
55144
55372
  if (env3)
55145
55373
  return env3;
55146
55374
  const dir = globalMmaDir();
55147
- for (const p of [join62(dir, "config", "core.json"), join62(dir, "config.json")]) {
55375
+ for (const p of [join63(dir, "config", "core.json"), join63(dir, "config.json")]) {
55148
55376
  try {
55149
55377
  const parsed = JSON.parse(readFileSync45(p, "utf-8"));
55150
55378
  if (typeof parsed.locale === "string")
@@ -55244,7 +55472,7 @@ async function main() {
55244
55472
  const prompt = program2.args.join(" ");
55245
55473
  const mmaDir = globalMmaDir();
55246
55474
  const { hasDomainFiles: hasDomainFiles2 } = await Promise.resolve().then(() => (init_domains(), exports_domains));
55247
- const virgin = !existsSync64(join62(mmaDir, "config.json")) && !hasDomainFiles2(mmaDir);
55475
+ const virgin = !existsSync64(join63(mmaDir, "config.json")) && !hasDomainFiles2(mmaDir);
55248
55476
  const { agent, config, baseDir, legacyDetected, contextProbe } = await bootstrap(undefined, projectDir, noAgentsMd, exitOnComplete, reasoningLevel, contextLog);
55249
55477
  if (verboseOpt)
55250
55478
  config.ui.verbosity = "verbose";
@@ -55335,7 +55563,7 @@ async function main() {
55335
55563
  if (onboardingStart === "init")
55336
55564
  process.env[MMA_ENV.START_INIT] = "1";
55337
55565
  const mmaDir = globalMmaDir();
55338
- const legacyConfigPath = join62(mmaDir, "config.json");
55566
+ const legacyConfigPath = join63(mmaDir, "config.json");
55339
55567
  let hasAnyConfig = existsSync64(legacyConfigPath);
55340
55568
  if (!hasAnyConfig) {
55341
55569
  try {
@@ -55343,7 +55571,7 @@ async function main() {
55343
55571
  hasAnyConfig = hasDomainFiles2(mmaDir);
55344
55572
  } catch {}
55345
55573
  }
55346
- const projectConfigPath = projectDir ? join62(projectDir, ".mmrc") : join62(process.cwd(), ".mmrc");
55574
+ const projectConfigPath = projectDir ? join63(projectDir, ".mmrc") : join63(process.cwd(), ".mmrc");
55347
55575
  if (!hasAnyConfig && !exitOnComplete && readEnv("POST_SETUP") !== "1") {
55348
55576
  getDefaultChannel().writeLine(`
55349
55577
  ${t("cli.first_run")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "2.14.0",
3
+ "version": "2.15.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": {