micro-models-agent 0.51.1 → 0.52.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/dist/cli/commands.js +162 -38
- package/dist/cli/completer.js +5 -5
- package/dist/cli/main.js +42 -54
- package/dist/cli/repl-commands.js +138 -38
- package/dist/cli/repl.js +175 -89
- package/dist/cli/run-result.js +11 -0
- package/dist/cli/security-commands.js +6 -6
- package/dist/cli/setup.js +21 -15
- package/dist/config/config.js +54 -27
- package/dist/config/defaults.js +17 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/index.js +2 -1
- package/dist/config/security.js +28 -8
- package/dist/core/agent.js +162 -30
- package/dist/core/bootstrap.js +94 -17
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/session-logger.js +60 -6
- package/dist/core/version.js +2 -0
- package/dist/i18n/en.json +120 -39
- package/dist/i18n/ru.json +91 -10
- package/dist/llm/openai-compat.js +191 -53
- package/dist/llm/orchestrator.js +5 -3
- package/dist/logger/app-logger.js +50 -4
- package/dist/main.js +1288 -635
- package/dist/modules/browser/session.js +4 -0
- package/dist/modules/certification/cli.js +58 -19
- package/dist/modules/certification/loader.js +2 -1
- package/dist/modules/certification/manifest.js +22 -14
- package/dist/modules/certification/runner.js +91 -5
- package/dist/modules/certification/scenarios.js +290 -7
- package/dist/modules/context/fact-extractor.js +6 -0
- package/dist/modules/context/manager.js +19 -2
- package/dist/modules/execution/audit-runners.js +61 -7
- package/dist/modules/execution/execution-plugin.js +219 -60
- package/dist/modules/execution/module.js +207 -18
- package/dist/modules/execution/moe-executor.js +33 -20
- package/dist/modules/execution/plan-store.js +39 -0
- package/dist/modules/execution/plan-tool.js +188 -19
- package/dist/modules/execution/planner.js +27 -23
- package/dist/modules/execution/stuck-detector.js +244 -8
- package/dist/modules/execution/tracker.js +8 -6
- package/dist/modules/execution/verifier.js +15 -2
- package/dist/modules/hallucination/detector.js +4 -0
- package/dist/modules/hallucination/factual.js +45 -5
- package/dist/modules/indexer/module.js +1 -0
- package/dist/modules/lsp/client.js +123 -12
- package/dist/modules/lsp/index.js +1 -1
- package/dist/modules/lsp/module.js +30 -2
- package/dist/modules/lsp/probe.js +11 -1
- package/dist/modules/lsp/startup-check.js +5 -2
- package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
- package/dist/modules/plugins/manager.js +57 -13
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +1 -0
- package/dist/modules/security/command-validator.js +14 -0
- package/dist/modules/security/encryption.js +6 -6
- package/dist/modules/security/network-validator.js +17 -0
- package/dist/modules/security/path-validator.js +22 -26
- package/dist/modules/session/store.js +10 -10
- package/dist/tools/approve.js +1 -0
- package/dist/tools/attach-image.js +12 -0
- package/dist/tools/bash.js +27 -4
- package/dist/tools/browser.js +1 -0
- package/dist/tools/chunk-query.js +1 -0
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/download-file.js +1 -0
- package/dist/tools/edit-file.js +2 -1
- package/dist/tools/enable-tools.js +1 -0
- package/dist/tools/executor.js +17 -7
- package/dist/tools/file-info.js +1 -0
- package/dist/tools/glob-tool.js +1 -0
- package/dist/tools/grep-tool.js +54 -13
- package/dist/tools/list-dir.js +1 -0
- package/dist/tools/load-skill.js +1 -0
- package/dist/tools/mcp-call.js +1 -0
- package/dist/tools/move-file.js +1 -0
- package/dist/tools/path-utils.js +51 -1
- package/dist/tools/pipeline-run.js +1 -0
- package/dist/tools/process-kill.js +11 -0
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +9 -0
- package/dist/tools/question.js +1 -0
- package/dist/tools/read-file.js +94 -6
- package/dist/tools/recall.js +1 -0
- package/dist/tools/remember.js +1 -0
- package/dist/tools/scope-check.js +7 -5
- package/dist/tools/search-history.js +1 -0
- package/dist/tools/subagent.js +4 -4
- package/dist/tools/web-browse.js +1 -0
- package/dist/tools/web-fetch.js +27 -6
- package/dist/tools/web-search.js +70 -43
- package/dist/tools/write-file.js +1 -0
- package/dist/ui/line-editor.js +142 -23
- package/dist/ui/line-math.js +8 -4
- package/dist/ui/renderer.js +57 -7
- package/package.json +50 -48
package/dist/core/agent.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { ProviderManager } from "../modules/providers/manager";
|
|
1
2
|
import { t } from "../i18n/index";
|
|
2
3
|
import { pc } from "../ui/colors";
|
|
3
4
|
import { PromptBuilder } from "./prompt-builder";
|
|
4
5
|
import { processRegistry } from "../modules/processes";
|
|
5
6
|
import { SessionLogger } from "./session-logger";
|
|
6
7
|
import { runWithMoE } from "./agent-moe";
|
|
7
|
-
import {
|
|
8
|
+
import { CostTracker } from "../modules/pricing/index";
|
|
8
9
|
const TOOL_RESULT_MAX_TOKENS_RATIO = 0.3;
|
|
9
10
|
const TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000;
|
|
10
11
|
const QUALITY_TRIGGER_THRESHOLD = 40;
|
|
@@ -13,6 +14,7 @@ const QUALITY_TRIGGER_THRESHOLD = 40;
|
|
|
13
14
|
* (observed: 56 compactions in ~28 min) and the compaction itself can't
|
|
14
15
|
* restore quality, so the agent burns the whole budget compacting. */
|
|
15
16
|
const FORCED_COMPACTION_COOLDOWN = 3;
|
|
17
|
+
const MAX_LLM_ERROR_RETRIES = 2;
|
|
16
18
|
/** True when the text looks like a raw JSON tool payload (garbage to display). */
|
|
17
19
|
function isToolCallJson(text) {
|
|
18
20
|
const trimmed = text.trim();
|
|
@@ -46,13 +48,25 @@ export class Agent {
|
|
|
46
48
|
shutdownRequested = false;
|
|
47
49
|
abortController = null;
|
|
48
50
|
lastCompactionShown = 0;
|
|
51
|
+
costTracker;
|
|
49
52
|
constructor(deps) {
|
|
50
53
|
this.deps = deps;
|
|
54
|
+
this.costTracker = new CostTracker(deps.config.model, deps.config.pricing);
|
|
51
55
|
}
|
|
52
56
|
/** Expose context manager for REPL image attachment and other direct access. */
|
|
53
57
|
get contextManager() {
|
|
54
58
|
return this.deps.contextManager;
|
|
55
59
|
}
|
|
60
|
+
callerProvenance() {
|
|
61
|
+
return {
|
|
62
|
+
provider: this.deps.config.provider?.type || "unknown",
|
|
63
|
+
model: this.deps.config.model,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Typed module accessor for CLI/REPL commands (e.g. /lsp). */
|
|
67
|
+
getModule(name) {
|
|
68
|
+
return this.deps.moduleRegistry?.get(name);
|
|
69
|
+
}
|
|
56
70
|
setScope() {
|
|
57
71
|
if (this.deps.scope) {
|
|
58
72
|
this.deps.toolExecutor.setScope(this.deps.scope);
|
|
@@ -168,15 +182,25 @@ export class Agent {
|
|
|
168
182
|
onPhase?.(phase);
|
|
169
183
|
}
|
|
170
184
|
async run(input, onChunk, onMeta, onTool, onPhase) {
|
|
185
|
+
// Reset shutdown flag from previous interrupt
|
|
186
|
+
this.shutdownRequested = false;
|
|
171
187
|
this.setScope();
|
|
172
188
|
const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager, baseDir, } = this.deps;
|
|
173
|
-
const slog = new SessionLogger(sessionManager, logger);
|
|
189
|
+
const slog = new SessionLogger(sessionManager, logger, () => this.callerProvenance());
|
|
174
190
|
if (sessionManager && !sessionManager.getActive()) {
|
|
175
191
|
sessionManager.create();
|
|
176
192
|
logger.debug(`Session started: ${sessionManager.getActive()}`);
|
|
177
193
|
}
|
|
178
194
|
if (!this.systemPromptAdded &&
|
|
179
195
|
!contextManager.getActiveHistory().some((m) => m.role === "system")) {
|
|
196
|
+
// Log session startup diagnostics (environment, plugins, skills) as the
|
|
197
|
+
// first entry in session.jsonl so the file is self-describing for
|
|
198
|
+
// post-mortem analysis. LSP probe is logged separately from repl.ts
|
|
199
|
+
// since it may complete after this point. Dedup: logSessionStart
|
|
200
|
+
// checks for existing session_start entry to avoid double-logging.
|
|
201
|
+
slog.logSessionStart({
|
|
202
|
+
environment: this.deps.envReport,
|
|
203
|
+
});
|
|
180
204
|
const lazy = this.deps.lazyPromptBlocks ? await this.deps.lazyPromptBlocks() : [];
|
|
181
205
|
if (lazy.length > 0) {
|
|
182
206
|
this.deps.promptBlocks.push(...lazy);
|
|
@@ -193,18 +217,6 @@ export class Agent {
|
|
|
193
217
|
sessionManager: sessionManager?.getActiveMeta(),
|
|
194
218
|
contextManager,
|
|
195
219
|
});
|
|
196
|
-
if (config.session?.baselineCheck !== false) {
|
|
197
|
-
const verifier = new StepVerifier(baseDir);
|
|
198
|
-
verifier
|
|
199
|
-
.runTypeCheck()
|
|
200
|
-
.then((tc) => {
|
|
201
|
-
if (!tc.passed) {
|
|
202
|
-
logger.warn(`Baseline typecheck has issues: ${tc.message?.slice(0, 500)}`);
|
|
203
|
-
onMeta?.(pc.yellow(`\n⚠ Baseline typecheck has issues\n`));
|
|
204
|
-
}
|
|
205
|
-
})
|
|
206
|
-
.catch(() => { });
|
|
207
|
-
}
|
|
208
220
|
}
|
|
209
221
|
// A new user message starts a fresh compaction interval — iterations
|
|
210
222
|
// from the previous turn must not trigger a compaction on iteration 1
|
|
@@ -215,20 +227,50 @@ export class Agent {
|
|
|
215
227
|
}
|
|
216
228
|
contextManager.addMessage({ role: "user", content: input });
|
|
217
229
|
slog.logUser(input);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
// Turn lifecycle for plugins: count tool events without touching the loop,
|
|
231
|
+
// and dispatch onTurnEnd exactly once — success, error or interrupt.
|
|
232
|
+
const startedAt = Date.now();
|
|
233
|
+
let toolCalls = 0;
|
|
234
|
+
const countTool = (ev) => {
|
|
235
|
+
if (ev.type === "end")
|
|
236
|
+
toolCalls++;
|
|
237
|
+
onTool?.(ev);
|
|
238
|
+
};
|
|
239
|
+
const emitTurnEnd = (result, err) => {
|
|
240
|
+
const interrupted = (err instanceof Error && err.name === "AbortError") || this.shutdownRequested;
|
|
241
|
+
pluginManager.runOnTurnEnd?.({
|
|
223
242
|
logger,
|
|
224
|
-
|
|
225
|
-
|
|
243
|
+
sessionManager: sessionManager?.getActiveMeta(),
|
|
244
|
+
contextManager,
|
|
245
|
+
}, {
|
|
246
|
+
success: !!result && result.success !== false && !err,
|
|
247
|
+
durationMs: Date.now() - startedAt,
|
|
248
|
+
textLength: (result?.text || "").length,
|
|
249
|
+
toolCalls,
|
|
250
|
+
interrupted: interrupted || undefined,
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
const result = config.moe?.enabled === true
|
|
255
|
+
? await runWithMoE({
|
|
256
|
+
config,
|
|
257
|
+
llmProvider,
|
|
258
|
+
toolExecutor,
|
|
259
|
+
logger,
|
|
260
|
+
baseDir: this.deps.baseDir,
|
|
261
|
+
}, input, () => this.executeSingleAgentLoop(input, onChunk, onMeta, countTool, onPhase), { onMeta, onTool: countTool, onPhase })
|
|
262
|
+
: await this.executeSingleAgentLoop(input, onChunk, onMeta, countTool, onPhase);
|
|
263
|
+
emitTurnEnd(result);
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
emitTurnEnd(null, err);
|
|
268
|
+
throw err;
|
|
226
269
|
}
|
|
227
|
-
return this.executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase);
|
|
228
270
|
}
|
|
229
271
|
async executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase) {
|
|
230
272
|
const { config, llmProvider, toolExecutor, pluginManager, contextManager, hallucinationDetector, logger, sessionManager, baseDir, } = this.deps;
|
|
231
|
-
const slog = new SessionLogger(sessionManager, logger);
|
|
273
|
+
const slog = new SessionLogger(sessionManager, logger, () => this.callerProvenance());
|
|
232
274
|
this.abortController = new AbortController();
|
|
233
275
|
let iteration = 0;
|
|
234
276
|
let lastText = "";
|
|
@@ -263,6 +305,7 @@ export class Agent {
|
|
|
263
305
|
let suppressRepetitionRetry = false;
|
|
264
306
|
let repeatedToolCount = 0;
|
|
265
307
|
const MAX_REPEATED_TOOL_CALLS = 2;
|
|
308
|
+
let llmErrorRetries = 0;
|
|
266
309
|
// Account for tool definitions in context budget (they're sent via body.tools, not messages)
|
|
267
310
|
// Tool definitions are recomputed each iteration so `enable_tools`
|
|
268
311
|
// (which mutates the shared activeToolTags array) can grow the
|
|
@@ -295,6 +338,8 @@ export class Agent {
|
|
|
295
338
|
if (result) {
|
|
296
339
|
logger.debug("Context compacted");
|
|
297
340
|
slog.logCompaction({ reason: "interval", iteration, ...result });
|
|
341
|
+
// Seed hallucination checker with files from compaction summary
|
|
342
|
+
hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
298
343
|
}
|
|
299
344
|
}
|
|
300
345
|
const currentTokens = contextManager.getEstimatedTokens();
|
|
@@ -312,6 +357,7 @@ export class Agent {
|
|
|
312
357
|
iteration,
|
|
313
358
|
...result,
|
|
314
359
|
});
|
|
360
|
+
hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
315
361
|
}
|
|
316
362
|
}
|
|
317
363
|
if (currentTokens > budget.history) {
|
|
@@ -323,6 +369,7 @@ export class Agent {
|
|
|
323
369
|
iteration,
|
|
324
370
|
...result,
|
|
325
371
|
});
|
|
372
|
+
hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
326
373
|
}
|
|
327
374
|
}
|
|
328
375
|
this.refreshSystemPrompt();
|
|
@@ -393,6 +440,21 @@ export class Agent {
|
|
|
393
440
|
logger.info("LLM call aborted (interrupt)");
|
|
394
441
|
break;
|
|
395
442
|
}
|
|
443
|
+
// Recoverable LLM errors (e.g. the completion hit the token limit
|
|
444
|
+
// mid-tool_call): feed the localized reason back into context so the
|
|
445
|
+
// model can adapt (split the write into smaller chunks) instead of
|
|
446
|
+
// the whole session dying. Bounded to avoid infinite loops.
|
|
447
|
+
if (err?.recoverableLlm && llmErrorRetries < MAX_LLM_ERROR_RETRIES) {
|
|
448
|
+
llmErrorRetries++;
|
|
449
|
+
logger.warn(`Recoverable LLM error, feeding back (${llmErrorRetries}/${MAX_LLM_ERROR_RETRIES}): ${err.message}`);
|
|
450
|
+
slog.logError(err.message);
|
|
451
|
+
pluginManager.runOnError({ iteration, logger, contextManager }, err);
|
|
452
|
+
contextManager.addMessage({
|
|
453
|
+
role: "user",
|
|
454
|
+
content: `<system-summary>${err.message}</system-summary>`,
|
|
455
|
+
});
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
396
458
|
logger.logLLMResponse(config.model, textContent.length, Date.now() - llmStart, err.message, "agent");
|
|
397
459
|
logger.error(`LLM call failed: ${err.message}`);
|
|
398
460
|
slog.logError(err.message);
|
|
@@ -430,6 +492,7 @@ export class Agent {
|
|
|
430
492
|
source,
|
|
431
493
|
durationMs: Date.now() - llmStart,
|
|
432
494
|
});
|
|
495
|
+
this.costTracker.record(prompt, completion, this.callerProvenance().provider);
|
|
433
496
|
}
|
|
434
497
|
if (this.shutdownRequested) {
|
|
435
498
|
break;
|
|
@@ -497,6 +560,10 @@ export class Agent {
|
|
|
497
560
|
});
|
|
498
561
|
const summaries = [];
|
|
499
562
|
let anyToolFailed = false;
|
|
563
|
+
// Informational read-only tools whose "failure" is expected behavior
|
|
564
|
+
// (e.g. file_info "Not found" after delete_file) — excluded from
|
|
565
|
+
// per-tool failure counting and consecutive failure tracking.
|
|
566
|
+
const infoTools = new Set(["file_info"]);
|
|
500
567
|
for (const call of toolCalls) {
|
|
501
568
|
this.setScope();
|
|
502
569
|
const startTime = Date.now();
|
|
@@ -505,12 +572,19 @@ export class Agent {
|
|
|
505
572
|
args: call.arguments,
|
|
506
573
|
});
|
|
507
574
|
pluginManager.runOnToolStart({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments });
|
|
508
|
-
|
|
575
|
+
const toolIcon = this.deps.toolExecutor.getRegistry().get(call.name)?.icon;
|
|
576
|
+
onTool?.({ type: "start", tool: call.name, args: call.arguments, icon: toolIcon });
|
|
509
577
|
slog.logToolCall(call, iteration);
|
|
510
578
|
const tokensBeforeTool = contextManager.getEstimatedTokens();
|
|
511
579
|
const result = await toolExecutor.execute(call, this.abortController?.signal);
|
|
580
|
+
// Interrupt during the tool call: the session is already shutting
|
|
581
|
+
// down (Esc/Ctrl+C), so stop immediately — no rendering, no session
|
|
582
|
+
// log writes, no context update for a result nobody will see. This
|
|
583
|
+
// fixes the "Replaced in ..." lines appearing AFTER "Session ended".
|
|
584
|
+
if (this.shutdownRequested)
|
|
585
|
+
break;
|
|
512
586
|
const duration = Date.now() - startTime;
|
|
513
|
-
if (!result.success)
|
|
587
|
+
if (!result.success && !infoTools.has(call.name))
|
|
514
588
|
anyToolFailed = true;
|
|
515
589
|
if (result.success && call.arguments.path) {
|
|
516
590
|
const filePath = String(call.arguments.path);
|
|
@@ -522,8 +596,14 @@ export class Agent {
|
|
|
522
596
|
}
|
|
523
597
|
}
|
|
524
598
|
pluginManager.runOnToolEnd({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
|
|
525
|
-
|
|
526
|
-
|
|
599
|
+
// Presence check, not truthiness: read_file returns display ""
|
|
600
|
+
// on a full read to suppress output entirely (the tool header
|
|
601
|
+
// already shows the path) — falling back on empty string would
|
|
602
|
+
// dump the whole file content into the REPL.
|
|
603
|
+
if (result.display !== undefined) {
|
|
604
|
+
if (result.display.length > 0) {
|
|
605
|
+
onMeta?.("\n" + result.display + "\n");
|
|
606
|
+
}
|
|
527
607
|
}
|
|
528
608
|
else {
|
|
529
609
|
const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, result.output);
|
|
@@ -551,6 +631,7 @@ export class Agent {
|
|
|
551
631
|
duration,
|
|
552
632
|
error: !result.success,
|
|
553
633
|
ctxDelta: tokensAfterTool - tokensBeforeTool,
|
|
634
|
+
costUsd: this.costTracker.total,
|
|
554
635
|
});
|
|
555
636
|
summaries.push(`[Tool: ${call.name} (${JSON.stringify(call.arguments)}) → ${truncatedOutput.slice(0, 200)}]`);
|
|
556
637
|
if (config.session.autoSave) {
|
|
@@ -565,6 +646,7 @@ export class Agent {
|
|
|
565
646
|
iteration,
|
|
566
647
|
...result,
|
|
567
648
|
});
|
|
649
|
+
hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
568
650
|
}
|
|
569
651
|
}
|
|
570
652
|
// Track per-tool failure counts (NOT just consecutive) so a tool
|
|
@@ -572,7 +654,7 @@ export class Agent {
|
|
|
572
654
|
// is still learned from (observed: LSP spawn npx ENOENT failed
|
|
573
655
|
// 8x in one session, never consecutively, so it never reached
|
|
574
656
|
// memory via the 5-consecutive-failures path).
|
|
575
|
-
if (!result.success) {
|
|
657
|
+
if (!result.success && !infoTools.has(call.name)) {
|
|
576
658
|
const key = call.name;
|
|
577
659
|
const prev = toolFailureCounts.get(key) ?? { count: 0, error: "" };
|
|
578
660
|
prev.count++;
|
|
@@ -588,6 +670,8 @@ export class Agent {
|
|
|
588
670
|
}
|
|
589
671
|
}
|
|
590
672
|
}
|
|
673
|
+
if (this.shutdownRequested)
|
|
674
|
+
break;
|
|
591
675
|
if (anyToolFailed) {
|
|
592
676
|
consecutiveToolFailures++;
|
|
593
677
|
}
|
|
@@ -807,6 +891,8 @@ export class Agent {
|
|
|
807
891
|
promptTokens: usageTokens.prompt,
|
|
808
892
|
completionTokens: usageTokens.completion,
|
|
809
893
|
totalTokens: usageTokens.total,
|
|
894
|
+
totalCost: this.costTracker.total,
|
|
895
|
+
costBreakdown: this.costTracker.breakdown(),
|
|
810
896
|
compactionCount: contextManager.getCompactionCount(),
|
|
811
897
|
contextQuality: contextManager.getQuality(),
|
|
812
898
|
};
|
|
@@ -825,6 +911,8 @@ export class Agent {
|
|
|
825
911
|
promptTokens: usageTokens.prompt,
|
|
826
912
|
completionTokens: usageTokens.completion,
|
|
827
913
|
totalTokens: usageTokens.total,
|
|
914
|
+
totalCost: this.costTracker.total,
|
|
915
|
+
costBreakdown: this.costTracker.breakdown(),
|
|
828
916
|
compactionCount: contextManager.getCompactionCount(),
|
|
829
917
|
contextQuality: contextManager.getQuality(),
|
|
830
918
|
};
|
|
@@ -843,13 +931,56 @@ export class Agent {
|
|
|
843
931
|
contextWindow: config.contextWindow,
|
|
844
932
|
retry: config.retry,
|
|
845
933
|
rateLimits: config.security?.rateLimits,
|
|
934
|
+
maxCompletionTokens: config.provider.entries?.find((e) => e.label === config.provider.active)?.maxCompletionTokens ??
|
|
935
|
+
config.provider.maxCompletionTokens,
|
|
846
936
|
});
|
|
847
937
|
this.deps.llmProvider = newProvider;
|
|
848
938
|
this.deps.toolExecutor.updateProvider(newProvider);
|
|
849
|
-
this.deps.toolExecutor.ctx.llmProvider = newProvider;
|
|
850
939
|
const newTokenCounter = new TokenCounter(config.model);
|
|
851
940
|
this.deps.contextManager.resize(config.contextWindow, config.contextBudget, newTokenCounter);
|
|
852
941
|
this.deps.config = config;
|
|
942
|
+
this.costTracker.setModel(config.model);
|
|
943
|
+
}
|
|
944
|
+
setProvider(name, model) {
|
|
945
|
+
const manager = new ProviderManager(this.deps.config.provider, {
|
|
946
|
+
contextWindow: this.deps.config.contextWindow,
|
|
947
|
+
retry: this.deps.config.retry,
|
|
948
|
+
rateLimits: this.deps.config.security?.rateLimits,
|
|
949
|
+
});
|
|
950
|
+
manager.switch(name, model);
|
|
951
|
+
const providerCfg = manager.toConfig();
|
|
952
|
+
this.deps.config.provider = providerCfg;
|
|
953
|
+
this.deps.llmProvider = manager.active;
|
|
954
|
+
this.deps.toolExecutor.updateProvider(manager.active);
|
|
955
|
+
this.deps.toolExecutor.ctx.llmProvider = manager.active;
|
|
956
|
+
}
|
|
957
|
+
listProviders() {
|
|
958
|
+
const manager = new ProviderManager(this.deps.config.provider, {
|
|
959
|
+
contextWindow: this.deps.config.contextWindow,
|
|
960
|
+
retry: this.deps.config.retry,
|
|
961
|
+
rateLimits: this.deps.config.security?.rateLimits,
|
|
962
|
+
});
|
|
963
|
+
const activeName = this.deps.config.provider.active;
|
|
964
|
+
return manager.list().map((e) => ({
|
|
965
|
+
...e,
|
|
966
|
+
active: e.label === activeName || e.type === activeName,
|
|
967
|
+
}));
|
|
968
|
+
}
|
|
969
|
+
/** Build (or get cached) the provider for a named entry — health checks, routing. */
|
|
970
|
+
getProviderFor(name) {
|
|
971
|
+
try {
|
|
972
|
+
const manager = new ProviderManager(this.deps.config.provider, {
|
|
973
|
+
contextWindow: this.deps.config.contextWindow,
|
|
974
|
+
retry: this.deps.config.retry,
|
|
975
|
+
rateLimits: this.deps.config.security?.rateLimits,
|
|
976
|
+
});
|
|
977
|
+
manager.setModel(this.deps.config.model);
|
|
978
|
+
manager.switch(name);
|
|
979
|
+
return manager.active;
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
return undefined;
|
|
983
|
+
}
|
|
853
984
|
}
|
|
854
985
|
setContext(messages) {
|
|
855
986
|
const { contextManager } = this.deps;
|
|
@@ -876,7 +1007,8 @@ export class Agent {
|
|
|
876
1007
|
if (killed > 0) {
|
|
877
1008
|
logger.info(`Killed ${killed} background process(es) on shutdown`);
|
|
878
1009
|
}
|
|
879
|
-
|
|
1010
|
+
// Note: do NOT close session log here — the session is still active.
|
|
1011
|
+
// Session log is closed only when the session actually ends (exit, session delete).
|
|
880
1012
|
pluginManager.runOnSessionEnd({
|
|
881
1013
|
logger,
|
|
882
1014
|
sessionManager: sessionManager?.getActiveMeta(),
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadConfig } from "../config/config";
|
|
2
2
|
import { Logger } from "../logger/app-logger";
|
|
3
|
-
import {
|
|
3
|
+
import { ProviderManager } from "../modules/providers/index";
|
|
4
|
+
import { FallbackProvider } from "../modules/providers/fallback";
|
|
4
5
|
import { ModelLoader } from "../llm/model-loader";
|
|
5
6
|
import { ToolRegistry, registerAllTools } from "../tools/index";
|
|
6
7
|
import { buildHiddenToolsBlock } from "../tools/hidden-tools-block";
|
|
@@ -18,7 +19,7 @@ import { SessionStore, SessionManager, SessionModule } from "../modules/session/
|
|
|
18
19
|
import { UserProfile } from "../modules/user-profile/profile";
|
|
19
20
|
import { SkillsLoader, SkillsModule } from "../modules/skills/index";
|
|
20
21
|
import { BrowserModule } from "../modules/browser/index";
|
|
21
|
-
import { LspModule, DEFAULT_LSP_CONFIG } from "../modules/lsp/index";
|
|
22
|
+
import { LspModule, FileOnlyLogger, DEFAULT_LSP_CONFIG } from "../modules/lsp/index";
|
|
22
23
|
import { runStartupHealthCheck } from "../modules/lsp/startup-check";
|
|
23
24
|
import { IndexerModule } from "../modules/indexer/index";
|
|
24
25
|
import { MCPModule } from "../modules/mcp/index";
|
|
@@ -30,6 +31,7 @@ import { homedir } from "os";
|
|
|
30
31
|
import { join, resolve } from "path";
|
|
31
32
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
32
33
|
import { readMmaVersion } from "./version";
|
|
34
|
+
import { collectEnvironment, logEnvironment } from "./environment";
|
|
33
35
|
export function buildSystemInfo(config, baseDir, profileCompressed) {
|
|
34
36
|
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
35
37
|
const isWin = profileCompressed.toLowerCase().includes("win32");
|
|
@@ -37,6 +39,8 @@ export function buildSystemInfo(config, baseDir, profileCompressed) {
|
|
|
37
39
|
`You are MMA v2, an AI coding agent for small models (${config.model}). Date: ${now}. Workspace: ${baseDir}. ${profileCompressed}.`,
|
|
38
40
|
`Reply in the user's language. Use tools for file ops (read/write/edit/delete), search (glob/grep), shell (bash), web, subagents, browser, MCP. Explain briefly if not obvious. On tool failure: analyze, fix the call, retry up to 2x with different approaches, then ask the user.`,
|
|
39
41
|
`Design: YAGNI (no unneeded code), KISS (simple over clever), DRY (reuse existing utilities).`,
|
|
42
|
+
`SCOPE DISCIPLINE: do ONLY what the user explicitly asked — no extra features, files, refactors, "improvements", or fixes beyond the request. Read-only requests ("расскажи", "покажи", "объясни", "check") mean READ-ONLY: inspect and answer, never create/modify/delete anything. If the task is ambiguous (what to create, where, which variant) or the request implies action on something you could not find — STOP and ask the user a short clarifying question in plain text instead of guessing.`,
|
|
43
|
+
`NOT FOUND ≠ MISSING PROJECT: a "not found" tool result means the PATH was wrong (typo, different location), not that the project does not exist. Before creating or scaffolding ANY project/files: first run list_dir on the working directory to see what is already there; if a user-named path is not found, list its parent directory to locate the real path. NEVER create a new project when the user asked about an existing one — inspect first, create only after confirming the workspace is empty AND the user asked for creation.`,
|
|
40
44
|
];
|
|
41
45
|
if (isWin) {
|
|
42
46
|
lines.push(`Windows (PowerShell): use list_dir/read_file/delete_file/create_dir tools instead of dir/type/del/mkdir. No PowerShell cmdlets (Get-Content, Select-Object, Write-Output), no head/tail/grep/cat. Use forward slashes in paths. CWD: ${baseDir}`);
|
|
@@ -59,6 +63,35 @@ export function buildSystemInfo(config, baseDir, profileCompressed) {
|
|
|
59
63
|
// postcss after the fix was on disk, while a fresh server on 5177 was
|
|
60
64
|
// clean) — the fix is only real when the USER's running server is clean.
|
|
61
65
|
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 — and confirm the fix against actual runtime output before calling "plan update step=N status=done". Check existing servers first with process_list/process_log: a stale dev server started BEFORE your change still runs the old pipeline and keeps failing even after the files on disk are fixed. Restart or kill it (process_kill) and verify the exact endpoint/port the user reported — a fresh server on a new port does NOT prove the user's error is gone.`);
|
|
66
|
+
// Security rules: when security is enabled the model should know the
|
|
67
|
+
// restrictions up front — a blocked call costs an iteration, while this
|
|
68
|
+
// block costs ~100 tokens once. Kept compact and capped so a long user
|
|
69
|
+
// blacklist cannot blow the system budget.
|
|
70
|
+
const sec = config.security;
|
|
71
|
+
if (sec?.enabled) {
|
|
72
|
+
const secLines = [`SECURITY RESTRICTIONS (enforced — blocked calls return "[SECURITY BLOCKED]", do not retry them):`];
|
|
73
|
+
if (sec.bash?.enabled && sec.bash.blacklist.length > 0) {
|
|
74
|
+
const cmds = sec.bash.blacklist.slice(0, 15).join(", ");
|
|
75
|
+
secLines.push(`Forbidden bash commands: ${cmds}${sec.bash.blacklist.length > 15 ? ", …" : ""}.`);
|
|
76
|
+
if (sec.bash.whitelist.length > 0) {
|
|
77
|
+
secLines.push(`Only whitelisted bash commands are allowed: ${sec.bash.whitelist.slice(0, 15).join(", ")}.`);
|
|
78
|
+
}
|
|
79
|
+
if (sec.bash.blockDangerousFlags) {
|
|
80
|
+
secLines.push(`Pipes/redirects/chaining (| > >> ; &&) in bash are blocked — one plain command per call.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (sec.paths?.enabled && sec.paths.denied.length > 0) {
|
|
84
|
+
const paths = sec.paths.denied.slice(0, 10).join(", ");
|
|
85
|
+
secLines.push(`Denied paths (read/write): ${paths}${sec.paths.denied.length > 10 ? ", …" : ""}.`);
|
|
86
|
+
}
|
|
87
|
+
if (sec.network?.enabled && sec.network.deniedDomains.length > 0) {
|
|
88
|
+
const domains = sec.network.deniedDomains.slice(0, 10).join(", ");
|
|
89
|
+
secLines.push(`Denied network domains: ${domains}${sec.network.deniedDomains.length > 10 ? ", …" : ""}.`);
|
|
90
|
+
}
|
|
91
|
+
if (secLines.length > 1) {
|
|
92
|
+
lines.push(...secLines);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
62
95
|
const hasMCP = config.mcpServers && Object.values(config.mcpServers).some((s) => s.enabled !== false);
|
|
63
96
|
if (hasMCP) {
|
|
64
97
|
lines.push(`MCP servers are available — use the named MCP tools (prefixed mcp__) to query external services.`);
|
|
@@ -66,9 +99,9 @@ export function buildSystemInfo(config, baseDir, profileCompressed) {
|
|
|
66
99
|
return lines.join("\n");
|
|
67
100
|
}
|
|
68
101
|
export async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplete) {
|
|
69
|
-
const dir = configDir || join(homedir(), ".mma");
|
|
102
|
+
const dir = configDir || process.env.MMA_CONFIG_DIR || join(homedir(), ".mma");
|
|
70
103
|
const projectConfigPath = projectDir ? join(projectDir, ".mmrc") : join(process.cwd(), ".mmrc");
|
|
71
|
-
const config = loadConfig({ configDir: dir, projectConfigPath });
|
|
104
|
+
const { config, legacyDetected } = loadConfig({ configDir: dir, projectConfigPath });
|
|
72
105
|
setLocale(config.locale);
|
|
73
106
|
// Update global audit notifier with config
|
|
74
107
|
try {
|
|
@@ -108,15 +141,36 @@ export async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplet
|
|
|
108
141
|
const profile = new UserProfile(join(dir));
|
|
109
142
|
profile.load() || profile.collect();
|
|
110
143
|
profile.save();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
144
|
+
// Multi-provider manager (Phase 2). Built from the (possibly legacy flat)
|
|
145
|
+
// config; the active provider is resolved through it. The initial provider
|
|
146
|
+
// comes from the manager so the active entry is honored on startup.
|
|
147
|
+
const providerManager = new ProviderManager(config.provider, {
|
|
115
148
|
contextWindow: config.contextWindow,
|
|
116
149
|
retry: config.retry,
|
|
117
150
|
rateLimits: config.security?.rateLimits,
|
|
118
151
|
});
|
|
152
|
+
providerManager.setModel(config.model);
|
|
153
|
+
let llmProvider = providerManager.active;
|
|
154
|
+
// Phase 4.2: transparent failover to the next configured provider on
|
|
155
|
+
// 429/5xx/network errors (only with multiple entries + fallback: true).
|
|
156
|
+
if (config.provider.fallback && providerManager.listNames().length > 1) {
|
|
157
|
+
llmProvider = new FallbackProvider(providerManager, (from, to, error) => {
|
|
158
|
+
logger.warn(`provider failover: ${from} -> ${to} (${error.message.slice(0, 120)})`);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
119
161
|
const baseDir = projectDir ? resolve(projectDir) : process.cwd();
|
|
162
|
+
// Cross-device diagnosis: log a full environment report (OS, runtime,
|
|
163
|
+
// engines, PATH tools, provider/retry, playwright) at every startup.
|
|
164
|
+
const envReport = collectEnvironment({
|
|
165
|
+
configDir: dir,
|
|
166
|
+
baseDir,
|
|
167
|
+
config,
|
|
168
|
+
scanTools: process.env.NODE_ENV !== "test",
|
|
169
|
+
});
|
|
170
|
+
logEnvironment(envReport, logger);
|
|
171
|
+
for (const warning of envReport.warnings) {
|
|
172
|
+
logger.warn(warning);
|
|
173
|
+
}
|
|
120
174
|
const projectMapCacheDir = join(baseDir, ".mma");
|
|
121
175
|
const indexerModule = new IndexerModule({
|
|
122
176
|
baseDir,
|
|
@@ -184,8 +238,6 @@ export async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplet
|
|
|
184
238
|
exitOnComplete,
|
|
185
239
|
contextManager,
|
|
186
240
|
activeToolTags,
|
|
187
|
-
sessionId: sessionManager.getActiveMeta()?.id,
|
|
188
|
-
sessionContext: sessionManager.getSessionContext() ?? undefined,
|
|
189
241
|
recursionDepth: 0,
|
|
190
242
|
fileOperationsCount: 0,
|
|
191
243
|
sessionHistory: {
|
|
@@ -213,17 +265,35 @@ export async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplet
|
|
|
213
265
|
},
|
|
214
266
|
},
|
|
215
267
|
};
|
|
268
|
+
// `sessionId`/`sessionContext` are exposed as LIVE getters over the session
|
|
269
|
+
// manager instead of being captured once at bootstrap. Capturing the values
|
|
270
|
+
// pinned them to the session active at startup, so after a REPL /new or
|
|
271
|
+
// /resume the new session's audit.jsonl entries were tagged with the
|
|
272
|
+
// PREVIOUS session's id (observed: ses_msyghz8w audit entries carrying
|
|
273
|
+
// ses_msyg892p_78kqhc) while sessionHistory kept reading the live meta.
|
|
274
|
+
// Everything that consumes ctx.sessionId — audit log, process registry,
|
|
275
|
+
// file-ops counters — now always sees the session that is actually active.
|
|
276
|
+
Object.defineProperty(toolCtx, "sessionId", {
|
|
277
|
+
get: () => sessionManager.getActiveMeta()?.id,
|
|
278
|
+
configurable: true,
|
|
279
|
+
});
|
|
280
|
+
Object.defineProperty(toolCtx, "sessionContext", {
|
|
281
|
+
get: () => sessionManager.getSessionContext() ?? undefined,
|
|
282
|
+
configurable: true,
|
|
283
|
+
});
|
|
216
284
|
const toolExecutor = new ToolExecutor(toolRegistry, toolCtx, pluginManager);
|
|
217
285
|
toolCtx.llmProvider = llmProvider;
|
|
218
286
|
toolCtx.toolExecutor = toolExecutor;
|
|
219
287
|
const hallucinationDetector = new HallucinationDetector(baseDir, llmProvider);
|
|
220
288
|
const moduleRegistry = new ModuleRegistry();
|
|
221
|
-
const execModule = new ExecutionModule(baseDir, config.stuckThreshold);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
289
|
+
const execModule = new ExecutionModule(baseDir, config.stuckThreshold, config.errorWebSearch?.threshold ?? 5);
|
|
290
|
+
// A plan in .mma/plans/active.json is PROJECT state, not session state —
|
|
291
|
+
// restore it in every session so `plan show` works and the checklist is
|
|
292
|
+
// visible. Nothing is printed unprompted: the REPL renders the checklist
|
|
293
|
+
// only on plan/todo tool events.
|
|
294
|
+
execModule.restorePlan();
|
|
226
295
|
moduleRegistry.register(execModule);
|
|
296
|
+
execModule.setHallucinationDetector(hallucinationDetector);
|
|
227
297
|
const sessionModule = new SessionModule(sessionManager);
|
|
228
298
|
moduleRegistry.register(sessionModule);
|
|
229
299
|
moduleRegistry.register(indexerModule);
|
|
@@ -242,7 +312,8 @@ export async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplet
|
|
|
242
312
|
pluginManager.register(browserPlugin);
|
|
243
313
|
}
|
|
244
314
|
}
|
|
245
|
-
const
|
|
315
|
+
const lspLogger = new FileOnlyLogger(logger);
|
|
316
|
+
const lspModule = new LspModule(config.lsp, undefined, lspLogger);
|
|
246
317
|
moduleRegistry.register(lspModule);
|
|
247
318
|
const lspPlugin = lspModule.getPlugin();
|
|
248
319
|
if (lspPlugin) {
|
|
@@ -257,7 +328,7 @@ export async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplet
|
|
|
257
328
|
// the time the user types in the interactive REPL).
|
|
258
329
|
let startupCheckBlock = null;
|
|
259
330
|
const startupCheckPromise = !exitOnComplete && process.env.NODE_ENV !== "test"
|
|
260
|
-
? runStartupHealthCheck(config.lsp ?? DEFAULT_LSP_CONFIG, baseDir)
|
|
331
|
+
? runStartupHealthCheck(config.lsp ?? DEFAULT_LSP_CONFIG, baseDir, { logger: lspLogger })
|
|
261
332
|
: Promise.resolve(null);
|
|
262
333
|
const moduleTools = moduleRegistry.collectToolDefinitions();
|
|
263
334
|
for (const tool of moduleTools) {
|
|
@@ -356,6 +427,7 @@ export async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplet
|
|
|
356
427
|
baseDir,
|
|
357
428
|
toolTags: activeToolTags,
|
|
358
429
|
promptBlocks,
|
|
430
|
+
envReport,
|
|
359
431
|
getDynamicPromptBlocks: () => {
|
|
360
432
|
const blocks = [];
|
|
361
433
|
const planBlock = execModule.getSystemPromptBlock();
|
|
@@ -387,6 +459,7 @@ export async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplet
|
|
|
387
459
|
},
|
|
388
460
|
sessionManager,
|
|
389
461
|
memoryStore,
|
|
462
|
+
moduleRegistry,
|
|
390
463
|
exitOnComplete,
|
|
391
464
|
};
|
|
392
465
|
const agent = new Agent(agentDeps);
|
|
@@ -397,8 +470,12 @@ export async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplet
|
|
|
397
470
|
sessionManager,
|
|
398
471
|
skillsModule,
|
|
399
472
|
pluginManager,
|
|
473
|
+
execModule,
|
|
474
|
+
toolCtx,
|
|
400
475
|
configDir: dir,
|
|
401
476
|
baseDir,
|
|
402
477
|
noAgentsMd: skipAgentsMd,
|
|
478
|
+
envReport,
|
|
479
|
+
legacyDetected,
|
|
403
480
|
};
|
|
404
481
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { collectEnvironment } from "./environment";
|
|
5
|
+
import { sanitizeLogMessage } from "../modules/security/data-sanitizer";
|
|
6
|
+
import { t } from "../i18n/index";
|
|
7
|
+
export const CRASH_LOG_DIR = join(homedir(), ".mma", "logs");
|
|
8
|
+
export const CRASH_LOG_FILE = "crash.jsonl";
|
|
9
|
+
/** Build a sanitized crash entry. Environment is captured at crash time (no tool scan). */
|
|
10
|
+
export function formatCrashEntry(type, err) {
|
|
11
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12
|
+
const stack = err instanceof Error && err.stack ? err.stack : message;
|
|
13
|
+
return {
|
|
14
|
+
ts: new Date().toISOString(),
|
|
15
|
+
type,
|
|
16
|
+
message: sanitizeLogMessage(message),
|
|
17
|
+
stack: sanitizeLogMessage(stack),
|
|
18
|
+
environment: collectEnvironment({ configDir: homedir(), scanTools: false }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function writeCrashEntry(dir, entry) {
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
appendFileSync(join(dir, CRASH_LOG_FILE), JSON.stringify(entry) + "\n", "utf-8");
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Best effort — never crash while reporting a crash
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let installed = false;
|
|
31
|
+
/**
|
|
32
|
+
* Global crash handlers. uncaughtException is logged + re-raised (exit 1 so the
|
|
33
|
+
* process never continues in a broken state); unhandledRejection is logged only
|
|
34
|
+
* (the event loop keeps running). Entries land in ~/.mma/logs/crash.jsonl.
|
|
35
|
+
*/
|
|
36
|
+
export function installCrashHandlers() {
|
|
37
|
+
if (installed)
|
|
38
|
+
return;
|
|
39
|
+
installed = true;
|
|
40
|
+
process.on("uncaughtException", (err) => {
|
|
41
|
+
const entry = formatCrashEntry("uncaughtException", err);
|
|
42
|
+
writeCrashEntry(CRASH_LOG_DIR, entry);
|
|
43
|
+
process.stderr.write(`${t("env.crash_stderr", { type: entry.type, message: entry.message })}\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
46
|
+
process.on("unhandledRejection", (reason) => {
|
|
47
|
+
const entry = formatCrashEntry("unhandledRejection", reason);
|
|
48
|
+
writeCrashEntry(CRASH_LOG_DIR, entry);
|
|
49
|
+
process.stderr.write(`${t("env.crash_stderr", { type: entry.type, message: entry.message })}\n`);
|
|
50
|
+
});
|
|
51
|
+
}
|