micro-models-agent 0.41.2 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +19 -9
  3. package/dist/cli/completer.js +37 -36
  4. package/dist/cli/index.js +2 -2
  5. package/dist/cli/main.js +23 -48
  6. package/dist/cli/repl-commands.js +12 -40
  7. package/dist/cli/repl.js +87 -217
  8. package/dist/cli/security-commands.js +7 -5
  9. package/dist/cli/setup.js +26 -8
  10. package/dist/config/config.js +5 -52
  11. package/dist/config/defaults.js +5 -29
  12. package/dist/config/experts.js +1 -1
  13. package/dist/config/index.js +3 -3
  14. package/dist/config/security.js +10 -3
  15. package/dist/core/agent-moe.js +10 -2
  16. package/dist/core/agent.js +82 -273
  17. package/dist/core/bootstrap.js +13 -80
  18. package/dist/core/index.js +2 -2
  19. package/dist/core/prompt-builder.js +2 -23
  20. package/dist/core/session-logger.js +4 -46
  21. package/dist/i18n/en.json +2 -75
  22. package/dist/i18n/ru.json +1 -74
  23. package/dist/index.js +1 -1
  24. package/dist/llm/image-utils.js +5 -4
  25. package/dist/llm/index.js +4 -4
  26. package/dist/llm/model-loader.js +6 -6
  27. package/dist/llm/openai-compat.js +34 -40
  28. package/dist/llm/orchestrator.js +29 -33
  29. package/dist/llm/response.js +9 -9
  30. package/dist/logger/app-logger.js +1 -1
  31. package/dist/logger/index.js +1 -1
  32. package/dist/main.js +648 -183
  33. package/dist/migration/backup.js +13 -13
  34. package/dist/migration/detect.js +11 -11
  35. package/dist/migration/index.js +2 -2
  36. package/dist/modules/browser/actions.js +4 -34
  37. package/dist/modules/browser/bridge-server.mjs +202 -202
  38. package/dist/modules/browser/cookie-store.js +6 -6
  39. package/dist/modules/browser/index.js +5 -7
  40. package/dist/modules/browser/module.js +7 -8
  41. package/dist/modules/browser/session.js +84 -87
  42. package/dist/modules/browser/snapshot.js +58 -92
  43. package/dist/modules/browser/types.js +1 -4
  44. package/dist/modules/certification/cli.js +4 -2
  45. package/dist/modules/certification/fact-checker.js +3 -1
  46. package/dist/modules/certification/loader.js +9 -3
  47. package/dist/modules/certification/runner.js +4 -1
  48. package/dist/modules/context/index.js +1 -1
  49. package/dist/modules/context/manager.js +86 -160
  50. package/dist/modules/execution/auditor.js +25 -177
  51. package/dist/modules/execution/module.js +544 -201
  52. package/dist/modules/execution/moe-executor.js +0 -25
  53. package/dist/modules/execution/plan-store.js +3 -1
  54. package/dist/modules/execution/plan-validator.js +10 -10
  55. package/dist/modules/execution/planner.js +1 -6
  56. package/dist/modules/execution/stuck-detector.js +10 -173
  57. package/dist/modules/execution/verifier.js +42 -86
  58. package/dist/modules/hallucination/confidence.js +1 -8
  59. package/dist/modules/hallucination/detector.js +5 -2
  60. package/dist/modules/hallucination/factual.js +64 -3
  61. package/dist/modules/hallucination/index.js +1 -1
  62. package/dist/modules/hallucination/js-identifiers.js +0 -190
  63. package/dist/modules/hallucination/llm-judge.js +3 -1
  64. package/dist/modules/indexer/cache.js +7 -9
  65. package/dist/modules/indexer/index.js +3 -3
  66. package/dist/modules/indexer/module.js +42 -95
  67. package/dist/modules/indexer/walker.js +17 -17
  68. package/dist/modules/lsp/client.js +31 -74
  69. package/dist/modules/lsp/config.js +33 -87
  70. package/dist/modules/lsp/index.js +3 -3
  71. package/dist/modules/lsp/module.js +21 -185
  72. package/dist/modules/mcp/module.js +6 -2
  73. package/dist/modules/memory/index.js +1 -1
  74. package/dist/modules/memory/module.js +23 -71
  75. package/dist/modules/memory/search.js +9 -11
  76. package/dist/modules/memory/store.js +13 -13
  77. package/dist/modules/pipelines/engine.js +10 -10
  78. package/dist/modules/pipelines/index.js +3 -3
  79. package/dist/modules/pipelines/parser.js +14 -17
  80. package/dist/modules/pipelines/template.js +1 -1
  81. package/dist/modules/plugins/builtin/lint-on-write.js +16 -21
  82. package/dist/modules/plugins/builtin/notify.js +2 -3
  83. package/dist/modules/plugins/index.js +1 -1
  84. package/dist/modules/plugins/loader.js +17 -59
  85. package/dist/modules/plugins/manager.js +17 -73
  86. package/dist/modules/processes/index.js +1 -1
  87. package/dist/modules/processes/registry.js +46 -135
  88. package/dist/modules/registry.js +2 -4
  89. package/dist/modules/security/audit-notifier.js +39 -39
  90. package/dist/modules/security/command-validator.js +8 -2
  91. package/dist/modules/security/data-sanitizer.js +9 -1
  92. package/dist/modules/security/encryption.js +56 -58
  93. package/dist/modules/security/network-validator.js +9 -1
  94. package/dist/modules/security/path-validator.js +3 -1
  95. package/dist/modules/security/security-policies.js +19 -3
  96. package/dist/modules/security/session-encryption.js +1 -1
  97. package/dist/modules/security/session-isolation.js +8 -8
  98. package/dist/modules/session/index.js +3 -3
  99. package/dist/modules/session/module.js +5 -5
  100. package/dist/modules/session/store.js +9 -3
  101. package/dist/modules/skills/module.js +2 -1
  102. package/dist/modules/updater/checker.js +6 -70
  103. package/dist/modules/updater/index.js +1 -2
  104. package/dist/modules/user-profile/compressor.js +2 -2
  105. package/dist/modules/user-profile/index.js +1 -1
  106. package/dist/modules/user-profile/profile.js +9 -9
  107. package/dist/tools/attach-image.js +1 -1
  108. package/dist/tools/bash.js +19 -178
  109. package/dist/tools/browser.js +29 -46
  110. package/dist/tools/executor.js +5 -4
  111. package/dist/tools/file-info.js +12 -13
  112. package/dist/tools/filter-tools.js +2 -9
  113. package/dist/tools/glob-tool.js +11 -11
  114. package/dist/tools/grep-tool.js +3 -1
  115. package/dist/tools/index.js +2 -13
  116. package/dist/tools/list-dir.js +17 -18
  117. package/dist/tools/load-skill.js +3 -1
  118. package/dist/tools/path-utils.js +4 -4
  119. package/dist/tools/pipeline-run.js +25 -25
  120. package/dist/tools/process-kill.js +11 -11
  121. package/dist/tools/process-list.js +22 -20
  122. package/dist/tools/process-log.js +18 -22
  123. package/dist/tools/question.js +3 -1
  124. package/dist/tools/read-file.js +2 -10
  125. package/dist/tools/recall.js +37 -44
  126. package/dist/tools/registry.js +4 -15
  127. package/dist/tools/remember.js +29 -29
  128. package/dist/tools/scope-check.js +9 -9
  129. package/dist/tools/subagent.js +9 -54
  130. package/dist/tools/user-input.js +1 -1
  131. package/dist/tools/web-browse.js +3 -3
  132. package/dist/tools/web-fetch.js +3 -3
  133. package/dist/tools/web-search.js +3 -3
  134. package/dist/tools/write-file.js +3 -1
  135. package/dist/ui/box.js +5 -1
  136. package/dist/ui/index.js +6 -6
  137. package/dist/ui/md-formatter.js +33 -33
  138. package/dist/ui/output.js +5 -5
  139. package/dist/ui/renderer.js +10 -15
  140. package/dist/ui/table.js +1 -1
  141. package/package.json +48 -48
  142. package/dist/cli/plugin-commands.js +0 -36
  143. package/dist/cli/run-result.js +0 -22
  144. package/dist/core/version.js +0 -24
  145. package/dist/modules/artifacts/store.js +0 -61
  146. package/dist/modules/browser/bridge-client.js +0 -199
  147. package/dist/modules/browser/bridge-path.js +0 -10
  148. package/dist/modules/browser/driver.js +0 -136
  149. package/dist/modules/context/chunk-query.js +0 -100
  150. package/dist/modules/context/fact-extractor.js +0 -162
  151. package/dist/modules/context/history.js +0 -15
  152. package/dist/modules/execution/audit-runners.js +0 -152
  153. package/dist/modules/execution/execution-plugin.js +0 -272
  154. package/dist/modules/execution/plan-tool.js +0 -508
  155. package/dist/modules/execution/windows-commands.js +0 -41
  156. package/dist/modules/indexer/project-profile.js +0 -183
  157. package/dist/modules/lsp/check-tool.js +0 -58
  158. package/dist/modules/lsp/command.js +0 -60
  159. package/dist/modules/lsp/probe.js +0 -76
  160. package/dist/modules/lsp/project-root.js +0 -32
  161. package/dist/modules/lsp/startup-check.js +0 -141
  162. package/dist/modules/processes/detect.js +0 -34
  163. package/dist/modules/skills/matcher.js +0 -27
  164. package/dist/modules/updater/module.js +0 -116
  165. package/dist/tools/chunk-query.js +0 -99
  166. package/dist/tools/download-file.js +0 -116
  167. package/dist/tools/enable-tools.js +0 -58
  168. package/dist/tools/hidden-tools-block.js +0 -37
  169. package/dist/ui/line-editor.js +0 -703
  170. package/dist/ui/line-math.js +0 -69
  171. package/dist/ui/plan-view.js +0 -103
@@ -1,5 +1,4 @@
1
1
  import { getMessageText } from "../../llm/provider";
2
- import { FactExtractor } from "./fact-extractor";
3
2
  const COMPACTION_INTERVAL = 15;
4
3
  const KEEP_LAST_N = 6;
5
4
  function summarizeArgs(args) {
@@ -20,28 +19,15 @@ function truncate(s, max) {
20
19
  return s;
21
20
  return s.slice(0, max - 3) + "...";
22
21
  }
23
- function looksLikeErrorPaste(text) {
24
- // Error markers.
25
- if (/(?:^|\s)ERROR|Error:|error TS\d+|Transform failed|\[plugin:|SyntaxError|Cannot find|Uncaught|exception/i.test(text)) {
26
- return true;
27
- }
28
- // Structural signals of pasted compiler/runtime output: source frames
29
- // ("270| }_s(...)"), path:line:col references, and stack-trace lines.
30
- if (/(?:^|\n)\s*\d+\s*\|/.test(text))
31
- return true;
32
- if (/[\w./\\-]+\.[a-z]{1,6}:\d+:\d+/.test(text))
33
- return true;
34
- if (/(?:^|\n)\s*at\s+\S+/.test(text))
35
- return true;
36
- return false;
37
- }
38
22
  export function extractTriedAndFailed(messages) {
39
23
  const failures = new Map();
40
24
  for (const msg of messages) {
41
25
  if (msg.role === "tool" && msg.name && msg.success === false) {
42
26
  const key = `${msg.name}:${summarizeArgs(msg.arguments)}`;
43
27
  const existing = failures.get(key);
44
- const errorText = truncate(typeof msg.content === "string" ? msg.content : getMessageText(msg.content), 100);
28
+ const errorText = truncate(typeof msg.content === "string"
29
+ ? msg.content
30
+ : getMessageText(msg.content), 100);
45
31
  if (existing) {
46
32
  existing.count++;
47
33
  }
@@ -66,38 +52,20 @@ export class ContextManager {
66
52
  peakTokens = 0;
67
53
  budget;
68
54
  compactionThreshold;
69
- facts = new FactExtractor();
70
- /** Latest real user instruction (skips <system-summary> blocks). */
71
- lastUserTask = "";
72
- /** First real user instruction of the session — survives resetUserTurn(). */
73
- sessionMission = "";
74
- /** Latest user message that looks like a pasted error/log (never a task). */
75
- lastUserFeedback = "";
55
+ fileFacts = [];
56
+ decisionFacts = [];
57
+ errorFacts = [];
58
+ static MAX_FACTS = 20;
76
59
  tokenCounter;
77
60
  pendingImageParts = [];
78
61
  toolTokens = 0;
79
62
  onCompact = null;
80
- /**
81
- * Optional short line appended to the compaction summary so the model knows
82
- * an active plan exists with progress. Wired from bootstrap to the execution
83
- * module — without it, a 9B model re-creates a plan from scratch right after
84
- * compaction (observed: ses_msvuao0h — plan_e07pb2 (1/6) was discarded for a
85
- * fresh plan_xzq9xe the same iteration the old plan was still visible).
86
- */
87
- planSummaryProvider = null;
88
63
  constructor(contextWindow, contextBudget, tokenCounter) {
89
64
  this.contextWindow = contextWindow;
90
65
  this.compactionThreshold = contextBudget?.compactionThreshold ?? 0.75;
91
66
  this.budget = this.calculateBudget(contextWindow, contextBudget);
92
67
  this.tokenCounter = tokenCounter ?? null;
93
68
  }
94
- /**
95
- * Provide a short "current plan" line for the compaction summary, or null
96
- * when no plan is active. Injected once at bootstrap (not per-turn state).
97
- */
98
- setPlanSummaryProvider(fn) {
99
- this.planSummaryProvider = fn;
100
- }
101
69
  calculateBudget(window, contextBudget) {
102
70
  if (contextBudget) {
103
71
  const systemPrompt = Math.floor(window * contextBudget.systemPrompt);
@@ -124,67 +92,27 @@ export class ContextManager {
124
92
  noteIteration() {
125
93
  this.iterationsSinceCompaction++;
126
94
  }
127
- /**
128
- * Start a fresh user turn. The 15-iteration compaction interval must not
129
- * leak across user messages — otherwise a turn that ends at iteration 14
130
- * forces a compaction on the very first iteration of the next user message
131
- * (observed: compaction fired on iteration 8 of a new turn, deleting the
132
- * user's freshly-sent task alongside 40+ old turns). Facts are kept.
133
- */
134
- resetUserTurn() {
135
- this.iterationsSinceCompaction = 0;
136
- // A new user message begins a new turn — drop the previous turn's task
137
- // so the fresh instruction becomes the one preserved at compaction. The
138
- // session mission survives: an error/log paste in the new turn must not
139
- // erase the original goal.
140
- this.lastUserTask = "";
141
- this.lastUserFeedback = "";
142
- }
143
95
  getQuality() {
144
96
  const usedTokens = this.getEstimatedTokens();
145
97
  const tokenLoad = Math.max(0, 1 - usedTokens / this.budget.history);
146
- // Recoverable penalty: each compaction loses some information, but the
147
- // penalty is CAPPED so cumulative compactions can never permanently pin
148
- // quality below the forced-compaction trigger (40%). Before this cap, a
149
- // session with ~7+ compactions clamped the term to 0 forever; combined
150
- // with heavy token load + error density, quality stuck below 40 and a
151
- // compaction fired on EVERY iteration (observed: 56 compactions in 28 min).
152
- const COMPACTION_PENALTY_FLOOR = 0.5;
153
- const compactionLoss = Math.max(COMPACTION_PENALTY_FLOOR, 1 - this.compactionCount * 0.15);
98
+ const compactionLoss = Math.max(0, 1 - this.compactionCount * 0.15);
154
99
  const msgCount = this.messages.length || 1;
155
- const errorDensity = Math.max(0, 1 - Math.min(1, this.facts.errorCount / msgCount));
100
+ const errorDensity = Math.max(0, 1 - Math.min(1, this.errorFacts.length / msgCount));
156
101
  const freshness = Math.max(0, 1 - this.iterationsSinceCompaction / COMPACTION_INTERVAL);
157
- const score = tokenLoad * 0.4 + compactionLoss * 0.25 + errorDensity * 0.2 + freshness * 0.15;
102
+ const score = tokenLoad * 0.4 +
103
+ compactionLoss * 0.25 +
104
+ errorDensity * 0.2 +
105
+ freshness * 0.15;
158
106
  return Math.round(Math.min(100, Math.max(0, score * 100)));
159
107
  }
160
108
  addMessage(msg) {
161
- // Track the task the agent is working on so it survives compaction. The
162
- // user's real instruction is the FIRST non-system-summary user message of
163
- // the turn; everything after it (tool results, retry nudges) are injected
164
- // `user` envelopes that must not overwrite it (observed: after compaction
165
- // the model had forgotten "удали /bicycles, почини стили" and re-read
166
- // files aimlessly for 40+ iterations).
167
- if (msg.role === "user") {
168
- const text = typeof msg.content === "string" ? msg.content : getMessageText(msg.content);
169
- if (text.trim() && !text.startsWith("<system-summary>") && !text.includes("[Compressed:")) {
170
- if (looksLikeErrorPaste(text) && (this.lastUserTask || this.sessionMission)) {
171
- this.lastUserFeedback = text.trim().slice(0, 400);
172
- }
173
- else if (!this.lastUserTask && !this.lastUserFeedback) {
174
- // No new task inside a turn that already captured an error/log paste:
175
- // the paste's follow-up messages are tool-result continuations, not a
176
- // fresh instruction, so the session mission must win at compaction.
177
- this.lastUserTask = text.trim();
178
- if (!this.sessionMission)
179
- this.sessionMission = text.trim();
180
- }
181
- }
182
- }
183
109
  // Auto-attach pending images to the next user message
184
110
  if (msg.role === "user" && this.pendingImageParts.length > 0) {
185
111
  const textPart = {
186
112
  type: "text",
187
- text: typeof msg.content === "string" ? msg.content : getMessageText(msg.content),
113
+ text: typeof msg.content === "string"
114
+ ? msg.content
115
+ : getMessageText(msg.content),
188
116
  };
189
117
  msg = {
190
118
  ...msg,
@@ -278,53 +206,27 @@ export class ContextManager {
278
206
  return totalTokens > this.budget.history * this.compactionThreshold;
279
207
  }
280
208
  compact() {
281
- // Capture the pre-compaction state BEFORE any counters reset or rise —
282
- // qualityBefore must reflect the context the model actually saw.
283
- const tokensBefore = this.getEstimatedTokens();
284
- const qualityBefore = this.getQuality();
285
- const messagesBefore = this.messages.length;
286
209
  // Reset the counter even when nothing to compact — otherwise
287
210
  // needsCompaction() returns true forever after 15 iterations with few messages.
288
211
  this.iterationsSinceCompaction = 0;
289
212
  if (this.messages.length <= KEEP_LAST_N * 2)
290
- return null;
213
+ return;
291
214
  this.compactionCount++;
292
215
  const cutoff = this.messages.length - KEEP_LAST_N * 2;
293
216
  const oldTurns = this.messages.slice(0, cutoff);
294
217
  const recentTurns = this.messages.slice(cutoff);
295
- this.facts.extract(oldTurns);
218
+ this.extractFacts(oldTurns);
296
219
  const parts = [];
297
220
  parts.push(`[Compressed: ${oldTurns.length} old turns removed]`);
298
- // Carry the user's actual instruction forward — the model must never lose
299
- // the task it is working on just because 40 turns got compacted away. The
300
- // per-turn task falls back to the session mission (first real instruction)
301
- // so a pasted error/log in the current turn cannot erase the goal.
302
- const task = (this.lastUserTask || this.sessionMission).slice(0, 400);
303
- if (task)
304
- parts.push(`[Task: ${task}]`);
305
- if (this.lastUserFeedback && !task.startsWith(this.lastUserFeedback)) {
306
- parts.push(`[User feedback: ${this.lastUserFeedback}]`);
221
+ if (this.fileFacts.length > 0) {
222
+ parts.push(`[Files: ${this.fileFacts.slice(-15).join("; ")}]`);
223
+ }
224
+ if (this.decisionFacts.length > 0) {
225
+ parts.push(`[Decisions: ${this.decisionFacts.slice(-5).join("; ")}]`);
226
+ }
227
+ if (this.errorFacts.length > 0) {
228
+ parts.push(`[Errors: ${this.errorFacts.slice(-3).join("; ")}]`);
307
229
  }
308
- // Orient the model after compaction: an active plan must not be re-created
309
- // from scratch (see planSummaryProvider doc).
310
- const planLine = this.planSummaryProvider?.();
311
- if (planLine)
312
- parts.push(`[Plan: ${planLine}]`);
313
- const filesLine = this.facts.filesLine();
314
- if (filesLine)
315
- parts.push(filesLine);
316
- const deletedLine = this.facts.deletedLine();
317
- if (deletedLine)
318
- parts.push(deletedLine);
319
- const readLine = this.facts.readLine();
320
- if (readLine)
321
- parts.push(readLine);
322
- const decisionsLine = this.facts.decisionsLine();
323
- if (decisionsLine)
324
- parts.push(decisionsLine);
325
- const errorsLine = this.facts.errorsLine();
326
- if (errorsLine)
327
- parts.push(errorsLine);
328
230
  const triedAndFailed = extractTriedAndFailed(oldTurns);
329
231
  if (triedAndFailed.length > 0) {
330
232
  const lines = triedAndFailed.map((t) => `- ${t.tool}(${t.args}): ${t.error} (failed ${t.count}x)`);
@@ -343,38 +245,69 @@ export class ContextManager {
343
245
  const text = getMessageText(m.content);
344
246
  return !text.startsWith("<system-summary>");
345
247
  });
346
- this.messages = [...(firstSystem ? [firstSystem] : []), summary, ...freshRecent];
248
+ this.messages = [
249
+ ...(firstSystem ? [firstSystem] : []),
250
+ summary,
251
+ ...freshRecent,
252
+ ];
347
253
  this.iterationsSinceCompaction = 0;
348
254
  if (this.onCompact) {
349
255
  this.onCompact(summary);
350
256
  }
351
- return {
352
- removedTurns: oldTurns.length,
353
- keptTurns: freshRecent.length,
354
- tokensBefore,
355
- tokensAfter: this.getEstimatedTokens(),
356
- qualityBefore,
357
- qualityAfter: this.getQuality(),
358
- messagesBefore,
359
- messagesAfter: this.messages.length,
360
- summary: this.compactedBlock ?? "",
361
- };
362
257
  }
363
- /**
364
- * Snapshot of the current context state — used to log the initial context
365
- * structure at session start and per-iteration context stats.
366
- */
367
- getSnapshot() {
368
- return {
369
- window: this.contextWindow,
370
- budget: { ...this.budget },
371
- tokens: this.getEstimatedTokens(),
372
- toolTokens: this.toolTokens,
373
- messageCount: this.messages.length,
374
- quality: this.getQuality(),
375
- compactionCount: this.compactionCount,
376
- iterationsSinceCompaction: this.iterationsSinceCompaction,
377
- };
258
+ extractFacts(turns) {
259
+ const filePatterns = [
260
+ /(?:Created|Updated|Written|Deleted|Moved) ([\w./\\-]+\.[a-z]+)/gi,
261
+ /Файл (?:создан|обновлён|записан|удалён|перемещён):? ([\w./\\-]+\.[a-z]+)/gi,
262
+ /file (?:created|updated|written|deleted|moved):? ([\w./\\-]+\.[a-z]+)/gi,
263
+ ];
264
+ const newFiles = [];
265
+ const newDecisions = [];
266
+ const newErrors = [];
267
+ for (const msg of turns) {
268
+ const content = getMessageText(msg.content);
269
+ if (msg.role === "tool") {
270
+ for (const pattern of filePatterns) {
271
+ for (const match of content.matchAll(pattern)) {
272
+ newFiles.push(match[1]);
273
+ }
274
+ }
275
+ if (content.includes("Plan:") && content.includes("[")) {
276
+ const planLine = content
277
+ .split("\n")
278
+ .find((line) => line.includes("Plan:"));
279
+ if (planLine)
280
+ newDecisions.push(planLine.trim());
281
+ }
282
+ }
283
+ if (msg.role === "assistant") {
284
+ if (content.includes("decided:") || content.includes("decision:")) {
285
+ const line = content
286
+ .split("\n")
287
+ .find((l) => l.includes("decided:") || l.includes("decision:"));
288
+ if (line)
289
+ newDecisions.push(line.trim().slice(0, 200));
290
+ }
291
+ }
292
+ if (content.includes("Error:") ||
293
+ content.includes("failed") ||
294
+ content.includes("Ошибка:") ||
295
+ content.includes("не удалось")) {
296
+ const line = content
297
+ .split("\n")
298
+ .find((l) => l.includes("Error:") ||
299
+ l.includes("failed") ||
300
+ l.includes("Ошибка:") ||
301
+ l.includes("не удалось"));
302
+ if (line)
303
+ newErrors.push(line.trim().slice(0, 250));
304
+ }
305
+ }
306
+ // Deduplicate and cap facts to prevent unbounded growth
307
+ const dedup = (arr) => [...new Set(arr)];
308
+ this.fileFacts = dedup([...this.fileFacts, ...newFiles]).slice(-ContextManager.MAX_FACTS);
309
+ this.decisionFacts = dedup([...this.decisionFacts, ...newDecisions]).slice(-ContextManager.MAX_FACTS);
310
+ this.errorFacts = dedup([...this.errorFacts, ...newErrors]).slice(-ContextManager.MAX_FACTS);
378
311
  }
379
312
  /**
380
313
  * Replace the system prompt in place (keeps it first) or prepend a new one.
@@ -392,23 +325,16 @@ export class ContextManager {
392
325
  getActiveHistory() {
393
326
  return [...this.messages];
394
327
  }
395
- /**
396
- * Reset everything that belongs to the current conversation turn.
397
- * Fact lists (FactExtractor) intentionally survive clear() — they are
398
- * session-scoped compaction state, not per-turn.
399
- */
400
328
  clear() {
401
329
  this.messages = [];
402
330
  this.compactedBlock = null;
403
331
  this.iterationsSinceCompaction = 0;
404
332
  this.compactionCount = 0;
405
333
  this.peakTokens = 0;
406
- this.lastUserTask = "";
407
- this.lastUserFeedback = "";
408
- this.sessionMission = "";
409
334
  }
410
335
  getEstimatedTokens() {
411
- return (this.messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0) + this.toolTokens);
336
+ return (this.messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0) +
337
+ this.toolTokens);
412
338
  }
413
339
  setToolTokens(tokens) {
414
340
  this.toolTokens = tokens;
@@ -1,218 +1,66 @@
1
- import { existsSync, readdirSync } from "fs";
2
- import { resolve, join, basename } from "path";
1
+ import { existsSync } from "fs";
2
+ import { resolve } from "path";
3
3
  import { t } from "../../i18n/index";
4
- import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
5
- import { SKIP_DIRS, findTestFile, findTypecheckRoot, hasTestStep, runTests, runTypecheck, } from "./audit-runners";
6
- export { parseTypecheckErrors } from "./audit-runners";
4
+ import { isJsMemberAccess } from "../hallucination/js-identifiers";
7
5
  const MASS_EDIT_THRESHOLD = 10;
8
- /** Subtree walk depth cap for file resolution. */
9
- const RESOLVE_MAX_DEPTH = 8;
10
- /**
11
- * Resolve a plan-step file token to an existing file.
12
- *
13
- * A step description frequently names a file by its bare basename
14
- * ("App.tsx") or a partially-prefixed path ("src/App.tsx") while the real
15
- * file lives in a nested project directory relative to the agent baseDir
16
- * (e.g. `bicycle-shop/src/App.tsx`). Naively resolving against baseDir
17
- * reports the file as missing — observed in session ses_mssl5izw where the
18
- * final audit gate rejected a fully-completed task with "1 files missing"
19
- * forever, sending the model into a read/retry loop.
20
- *
21
- * Strategy: exact resolve first, then a bounded subtree walk matching either
22
- * the exact basename or a path suffix (so both "App.tsx" and
23
- * "src/App.tsx" find "bicycle-shop/src/App.tsx").
24
- */
25
- export function findExistingFile(baseDir, filePath) {
26
- const direct = resolve(baseDir, filePath);
27
- if (existsSync(direct))
28
- return direct;
29
- const name = basename(filePath).toLowerCase();
30
- const suffix = filePath.replace(/\\/g, "/").toLowerCase();
31
- let found = null;
32
- const walk = (dir, depth) => {
33
- if (found || depth > RESOLVE_MAX_DEPTH)
34
- return;
35
- let entries;
36
- try {
37
- entries = readdirSync(dir, { withFileTypes: true });
38
- }
39
- catch {
40
- return;
41
- }
42
- for (const e of entries) {
43
- if (found)
44
- return;
45
- const full = join(dir, e.name);
46
- if (e.isDirectory()) {
47
- if (SKIP_DIRS.has(e.name))
48
- continue;
49
- walk(full, depth + 1);
50
- }
51
- else if (e.name.toLowerCase() === name ||
52
- full
53
- .replace(/\\/g, "/")
54
- .toLowerCase()
55
- .endsWith("/" + suffix)) {
56
- found = full;
57
- return;
58
- }
59
- }
60
- };
61
- walk(baseDir, 0);
62
- return found;
63
- }
64
6
  export class Auditor {
65
7
  baseDir;
66
8
  constructor(baseDir) {
67
9
  this.baseDir = baseDir;
68
10
  }
69
11
  async audit(plan) {
70
- // Only steps the model claims completed may require their named files to
71
- // exist. Skipped/pending steps must never contribute "missing files"
72
- // (observed: a skipped step's prose mention of "type.ts" kept the audit
73
- // gate rejecting a finished task with "1 files missing" forever).
74
- const doneCreate = plan.steps.filter((s) => s.status === "done" && s.kind !== "delete");
75
- const doneDelete = plan.steps.filter((s) => s.status === "done" && s.kind === "delete");
76
- const createText = doneCreate.map((s) => s.description.replace(/\([^)]*\)/g, " ")).join(" ");
77
- const deleteText = doneDelete.map((s) => s.description.replace(/\([^)]*\)/g, " ")).join(" ");
78
- // File tokens are extracted with the SAME shared filter as the
79
- // hallucination factual check (js-identifiers.ts). URLs are stripped
80
- // first so a hostname / https://host/repo.git in a step description
81
- // can never be reported as a missing file (observed: a git-remote
82
- // plan flagged `whom-pointing-jewel-maine.trycloudflare.com`).
83
- const createFiles = extractFileLikeTokens(stripUrls(createText));
84
- const deleteFiles = extractFileLikeTokens(stripUrls(deleteText));
12
+ const allStepText = plan.steps
13
+ .map((s) => s.description.replace(/\([^)]*\)/g, " "))
14
+ .join(" ");
15
+ const fileMatches = allStepText.match(/\b[\w./-]+\.[a-z]+/gi) || [];
16
+ // process.env / console.log in step descriptions are JS member access,
17
+ // not files drop them before existence checks (false "missing file"
18
+ // warnings).
19
+ const uniqueFiles = [
20
+ ...new Set(fileMatches.filter((f) => !isJsMemberAccess(f))),
21
+ ];
85
22
  const missingFiles = [];
86
23
  const existingFiles = [];
87
- const leftoverFiles = [];
88
- for (const filePath of createFiles) {
89
- const resolved = findExistingFile(this.baseDir, filePath);
90
- if (resolved) {
91
- // Keep the resolved absolute path: findTypecheckRoot walks up
92
- // from it to the nearest tsconfig, and a relative token would
93
- // resolve against the process cwd instead of baseDir.
94
- existingFiles.push(resolved);
24
+ for (const filePath of uniqueFiles) {
25
+ const resolved = resolve(this.baseDir, filePath);
26
+ if (existsSync(resolved)) {
27
+ existingFiles.push(filePath);
95
28
  }
96
29
  else {
97
30
  missingFiles.push(filePath);
98
31
  }
99
32
  }
100
- // A done delete-kind step requires its named files to be GONE.
101
- for (const filePath of deleteFiles) {
102
- if (findExistingFile(this.baseDir, filePath)) {
103
- leftoverFiles.push(filePath);
104
- }
105
- }
106
- // Real test run: when the plan includes a verification step, do not
107
- // trust the model's word (or exit codes) — actually execute the tests
108
- // and check the runner summary. This catches the "all 7 passing ✅"
109
- // final answers that hide 3 failing tests.
110
- let testRun = null;
111
- if (hasTestStep(plan) && findTestFile(this.baseDir)) {
112
- try {
113
- testRun = await runTests(this.baseDir);
114
- }
115
- catch {
116
- testRun = null; // infra failure — don't block on it
117
- }
118
- }
119
- // Real typecheck: the audit must catch "the build is broken" even when
120
- // every file exists (observed: deleting .js files left the .tsx sources
121
- // with 11 tsc errors and the agent still declared success). Runs only
122
- // when a tsconfig.json is reachable from baseDir or the resolved step
123
- // files AND the file checks already passed — an audit that is doomed by
124
- // missing/leftover files must not pay a 90s tsc spawn. Inconclusive
125
- // results never block.
126
- let typecheckError = null;
127
- if (missingFiles.length === 0 && leftoverFiles.length === 0) {
128
- const root = findTypecheckRoot(this.baseDir, existingFiles);
129
- if (root) {
130
- try {
131
- typecheckError = await runTypecheck(root);
132
- }
133
- catch {
134
- typecheckError = null;
135
- }
136
- }
137
- }
138
33
  const doneSteps = plan.steps.filter((s) => s.status === "done").length;
139
- // done + skipped are both deliberate terminal states: `done` claims the
140
- // work, `skipped` documents a decision that the step is not needed (the
141
- // deliverable gate's documented escape hatch). Neither is a lie the
142
- // audit must catch — a plan with a legitimately skipped step must be
143
- // able to pass (observed: skipped step 1 kept the task rejected forever).
144
- const terminalSteps = plan.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
145
34
  const totalSteps = plan.steps.length;
146
- const auditedFiles = [...new Set([...createFiles, ...deleteFiles])];
147
35
  let massEditWarning = null;
148
- if (auditedFiles.length > MASS_EDIT_THRESHOLD) {
36
+ if (uniqueFiles.length > MASS_EDIT_THRESHOLD) {
149
37
  massEditWarning = t("exec.mass_edit_warning", {
150
- count: String(auditedFiles.length),
38
+ count: String(uniqueFiles.length),
151
39
  });
152
40
  }
153
- const testsFailing = testRun !== null && !testRun.passed;
154
- const typecheckFailing = typecheckError !== null;
155
- const passed = missingFiles.length === 0 && leftoverFiles.length === 0 && !testsFailing && !typecheckFailing;
156
- // A "Task complete ✓" summary for an unfinished plan is a lie the model
157
- // reads as a green light (observed: audit gate injected "[✓] Task
158
- // complete: 1/3 steps done" while the plan had 2 pending steps). The
159
- // summary must only claim completion when every step is actually done.
160
- const stepsPending = terminalSteps < totalSteps;
41
+ const passed = missingFiles.length === 0;
161
42
  let summary;
162
- if (missingFiles.length > 0) {
163
- summary = t("exec.audit_fail", {
164
- done: doneSteps,
165
- total: totalSteps,
166
- files: missingFiles.length,
167
- });
168
- }
169
- else if (leftoverFiles.length > 0) {
170
- summary = t("exec.audit_leftovers", {
171
- done: doneSteps,
172
- total: totalSteps,
173
- files: leftoverFiles.length,
174
- });
175
- }
176
- else if (testsFailing) {
177
- summary = t("exec.audit_fail_tests", {
178
- done: doneSteps,
179
- total: totalSteps,
180
- failed: String(testRun.failed),
181
- passed: String(testRun.passedCount),
182
- detail: testRun.detail,
183
- });
184
- }
185
- else if (typecheckFailing) {
186
- summary = t("exec.audit_fail_typecheck", {
187
- done: doneSteps,
188
- total: totalSteps,
189
- missing: String(missingFiles.length),
190
- typeError: typecheckError,
191
- });
192
- }
193
- else if (stepsPending) {
194
- summary = t("exec.audit_pending", {
43
+ if (passed) {
44
+ summary = t("exec.audit_pass", {
195
45
  done: doneSteps,
196
46
  total: totalSteps,
47
+ files: existingFiles.length,
197
48
  });
198
49
  }
199
50
  else {
200
- summary = t("exec.audit_pass", {
51
+ summary = t("exec.audit_fail", {
201
52
  done: doneSteps,
202
53
  total: totalSteps,
203
- files: existingFiles.length,
54
+ files: missingFiles.length,
204
55
  });
205
56
  }
206
57
  return {
207
58
  passed,
208
59
  missingFiles,
209
- leftoverFiles,
210
60
  createdFiles: existingFiles,
211
61
  modifiedFiles: [],
212
62
  summary,
213
63
  massEditWarning,
214
- testRun,
215
- typecheckError,
216
64
  };
217
65
  }
218
66
  }