micro-models-agent 0.39.0 → 0.40.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 (96) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +116 -3
  3. package/dist/cli/main.js +35 -8
  4. package/dist/cli/repl-commands.js +633 -0
  5. package/dist/cli/repl.js +110 -611
  6. package/dist/cli/setup.js +32 -12
  7. package/dist/config/config.js +46 -30
  8. package/dist/config/defaults.js +10 -1
  9. package/dist/config/security.js +15 -8
  10. package/dist/core/agent-moe.js +24 -12
  11. package/dist/core/agent.js +281 -47
  12. package/dist/core/bootstrap.js +52 -36
  13. package/dist/core/session-logger.js +35 -2
  14. package/dist/core/workspace.js +76 -0
  15. package/dist/i18n/en.json +79 -15
  16. package/dist/i18n/index.js +12 -9
  17. package/dist/i18n/ru.json +79 -15
  18. package/dist/index.js +13 -13
  19. package/dist/llm/openai-compat.js +39 -10
  20. package/dist/logger/app-logger.js +83 -16
  21. package/dist/logger/file-log.js +151 -0
  22. package/dist/main.js +537 -284
  23. package/dist/modules/browser/bridge-server.mjs +113 -105
  24. package/dist/modules/browser/session.js +108 -60
  25. package/dist/modules/certification/cli.js +176 -0
  26. package/dist/modules/certification/fact-checker.js +84 -0
  27. package/dist/modules/certification/loader.js +111 -0
  28. package/dist/modules/certification/manifest.js +50 -0
  29. package/dist/modules/certification/runner.js +162 -0
  30. package/dist/modules/certification/scenarios.js +124 -0
  31. package/dist/modules/certification/types.js +1 -0
  32. package/dist/modules/context/manager.js +119 -10
  33. package/dist/modules/execution/auditor.js +33 -39
  34. package/dist/modules/execution/index.js +8 -6
  35. package/dist/modules/execution/module.js +474 -32
  36. package/dist/modules/execution/moe-executor.js +97 -40
  37. package/dist/modules/execution/plan-coverage.js +68 -0
  38. package/dist/modules/execution/plan-persister.js +46 -0
  39. package/dist/modules/execution/plan-store.js +159 -0
  40. package/dist/modules/execution/planner.js +63 -13
  41. package/dist/modules/execution/stuck-detector.js +252 -39
  42. package/dist/modules/execution/tracker.js +21 -7
  43. package/dist/modules/execution/verifier.js +46 -17
  44. package/dist/modules/hallucination/confidence.js +7 -2
  45. package/dist/modules/hallucination/consistency.js +8 -42
  46. package/dist/modules/hallucination/detector.js +26 -21
  47. package/dist/modules/hallucination/factual.js +170 -150
  48. package/dist/modules/hallucination/index.js +5 -4
  49. package/dist/modules/hallucination/js-identifiers.js +72 -0
  50. package/dist/modules/hallucination/llm-judge.js +103 -0
  51. package/dist/modules/index.js +5 -5
  52. package/dist/modules/lsp/client.js +235 -0
  53. package/dist/modules/lsp/config.js +81 -0
  54. package/dist/modules/lsp/index.js +3 -0
  55. package/dist/modules/lsp/module.js +68 -0
  56. package/dist/modules/lsp/types.js +1 -0
  57. package/dist/modules/mcp/client.js +8 -2
  58. package/dist/modules/memory/store.js +4 -0
  59. package/dist/modules/plugins/builtin/lint-on-write.js +143 -38
  60. package/dist/modules/processes/index.js +1 -2
  61. package/dist/modules/processes/registry.js +125 -35
  62. package/dist/modules/processes/runner.js +9 -110
  63. package/dist/modules/security/audit-log.js +30 -10
  64. package/dist/modules/security/command-validator.js +42 -16
  65. package/dist/modules/security/content-scanner.js +9 -8
  66. package/dist/modules/security/network-validator.js +2 -2
  67. package/dist/modules/security/path-validator.js +64 -10
  68. package/dist/modules/security/security-policies.js +221 -67
  69. package/dist/modules/security/session-encryption.js +42 -25
  70. package/dist/modules/session/manager.js +15 -10
  71. package/dist/modules/session/store.js +62 -8
  72. package/dist/modules/skills/index.js +2 -3
  73. package/dist/modules/skills/module.js +10 -23
  74. package/dist/tools/bash.js +287 -90
  75. package/dist/tools/create-dir.js +0 -1
  76. package/dist/tools/delete-file.js +0 -1
  77. package/dist/tools/edit-file.js +10 -8
  78. package/dist/tools/executor.js +57 -7
  79. package/dist/tools/grep-tool.js +51 -29
  80. package/dist/tools/index.js +55 -40
  81. package/dist/tools/load-skill.js +14 -18
  82. package/dist/tools/move-file.js +3 -2
  83. package/dist/tools/pipeline-run.js +1 -1
  84. package/dist/tools/read-file.js +15 -5
  85. package/dist/tools/search-history.js +42 -22
  86. package/dist/tools/subagent.js +21 -12
  87. package/dist/tools/web-browse.js +54 -25
  88. package/dist/tools/web-fetch.js +60 -34
  89. package/dist/tools/web-search.js +39 -20
  90. package/dist/tools/write-file.js +13 -10
  91. package/dist/ui/diff.js +9 -16
  92. package/dist/ui/renderer.js +69 -6
  93. package/package.json +48 -45
  94. package/dist/modules/context/history.js +0 -15
  95. package/dist/modules/processes/detect.js +0 -34
  96. package/dist/modules/skills/matcher.js +0 -27
@@ -1,27 +1,62 @@
1
- import { execSync } from 'child_process';
2
- import { existsSync, readFileSync } from 'fs';
3
- import { resolve, extname, join } from 'path';
4
- let projectTypeCheckPromise = null;
5
- let projectTypeCheckTimestamp = 0;
1
+ import { spawn, execSync } from "child_process";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { resolve, extname, join } from "path";
4
+ import { platform } from "os";
6
5
  const TYPE_CHECK_DEBOUNCE_MS = 2000;
6
+ // Per-file syntax checks are cached by (path, content hash): re-writing a file
7
+ // with identical content skips the (expensive) check. Content changes → cache
8
+ // miss → fresh check.
9
+ const syntaxCache = new Map();
10
+ function contentHash(content) {
11
+ let h = 5381;
12
+ for (let i = 0; i < content.length; i++) {
13
+ h = ((h << 5) + h + content.charCodeAt(i)) | 0;
14
+ }
15
+ return String(h);
16
+ }
17
+ let _winDecoder;
18
+ function getWinDecoder() {
19
+ if (_winDecoder === undefined) {
20
+ if (platform() !== "win32") {
21
+ _winDecoder = null;
22
+ }
23
+ else {
24
+ try {
25
+ const cpOut = execSync("chcp.com", {
26
+ encoding: "buffer",
27
+ timeout: 2000,
28
+ windowsHide: true,
29
+ }).toString("latin1");
30
+ const m = cpOut.match(/(\d+)/);
31
+ _winDecoder = m ? new TextDecoder("ibm" + m[1]) : null;
32
+ }
33
+ catch {
34
+ _winDecoder = null;
35
+ }
36
+ }
37
+ }
38
+ return _winDecoder ?? new TextDecoder("utf-8");
39
+ }
7
40
  export class LintOnWritePlugin {
8
- name = 'lint-on-write';
41
+ name = "lint-on-write";
9
42
  priority = 10;
43
+ _checkPromise = null;
44
+ _checkTimestamp = 0;
10
45
  async onAfterTool(ctx, call, result) {
11
- if (call.name !== 'write_file' && call.name !== 'edit_file') {
46
+ if (call.name !== "write_file" && call.name !== "edit_file") {
12
47
  return;
13
48
  }
14
49
  if (!result.success) {
15
50
  return;
16
51
  }
17
- const path = String(call.arguments.path || '');
52
+ const path = String(call.arguments.path || "");
18
53
  if (!path)
19
54
  return;
20
55
  const fullPath = resolve(ctx.baseDir, path);
21
56
  if (!existsSync(fullPath))
22
57
  return;
23
58
  const ext = extname(fullPath);
24
- const syntaxError = this.checkSyntax(fullPath, ext, ctx.baseDir);
59
+ const syntaxError = await this.checkSyntax(fullPath, ext, ctx.baseDir);
25
60
  if (syntaxError) {
26
61
  result.output += `\n\n[Syntax check failed]: ${syntaxError}`;
27
62
  return;
@@ -29,32 +64,50 @@ export class LintOnWritePlugin {
29
64
  await this.runProjectLint(ctx, result);
30
65
  await this.runProjectTypeCheck(ctx, result);
31
66
  }
32
- checkSyntax(filePath, ext, baseDir) {
33
- if (ext === '.ts' || ext === '.tsx') {
67
+ async checkSyntax(filePath, ext, baseDir) {
68
+ if (ext === ".ts" || ext === ".tsx") {
69
+ // Syntax-only check via bun's parser (~200ms) instead of `npx tsc`
70
+ // (~3-5s per write). Full type errors are still surfaced by the
71
+ // debounced project typecheck below when a tsconfig exists.
72
+ let content = "";
73
+ try {
74
+ content = readFileSync(filePath, "utf-8");
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ const hash = contentHash(content);
80
+ const cached = syntaxCache.get(filePath);
81
+ if (cached && cached.hash === hash) {
82
+ return cached.error;
83
+ }
34
84
  try {
35
- execSync(`npx tsc --noEmit --skipLibCheck "${filePath}"`, { cwd: baseDir, stdio: 'pipe', timeout: 10000 });
85
+ await runAsync(`bun build --no-bundle --target=bun "${filePath}"`, baseDir, 10000);
86
+ syntaxCache.set(filePath, { hash, error: null });
36
87
  return null;
37
88
  }
38
89
  catch (err) {
39
- if (err.status === 127 || err.message.includes('not found') || err.message.includes('ENOENT')) {
90
+ if (err.status === 127 ||
91
+ err.message.includes("not found") ||
92
+ err.message.includes("ENOENT")) {
40
93
  return null;
41
94
  }
42
- const stderr = err.stderr?.toString() || err.stdout?.toString() || '';
43
- if (stderr.includes('error TS')) {
44
- const firstError = stderr.split('\n').find((line) => line.includes('error TS')) || 'TypeScript syntax error';
45
- return firstError.trim();
46
- }
47
- return null;
95
+ const stderr = err.stderr?.toString() || err.stdout?.toString() || "";
96
+ const firstError = stderr.split("\n").find((line) => line.trim()) ||
97
+ "TypeScript syntax error";
98
+ syntaxCache.set(filePath, { hash, error: firstError.trim() });
99
+ return firstError.trim();
48
100
  }
49
101
  }
50
- if (ext === '.js' || ext === '.jsx') {
102
+ if (ext === ".js" || ext === ".jsx") {
51
103
  try {
52
- execSync(`node --check "${filePath}"`, { cwd: baseDir, stdio: 'pipe', timeout: 5000 });
104
+ await runAsync(`node --check "${filePath}"`, baseDir, 5000);
53
105
  return null;
54
106
  }
55
107
  catch (err) {
56
- const stderr = err.stderr?.toString() || '';
57
- const firstError = stderr.split('\n').find((line) => line.trim()) || 'JavaScript syntax error';
108
+ const stderr = err.stderr?.toString() || "";
109
+ const firstError = stderr.split("\n").find((line) => line.trim()) ||
110
+ "JavaScript syntax error";
58
111
  return firstError.trim();
59
112
  }
60
113
  }
@@ -62,18 +115,18 @@ export class LintOnWritePlugin {
62
115
  }
63
116
  async runProjectLint(ctx, result) {
64
117
  try {
65
- const packageJsonPath = join(ctx.baseDir, 'package.json');
118
+ const packageJsonPath = join(ctx.baseDir, "package.json");
66
119
  if (!existsSync(packageJsonPath)) {
67
120
  return;
68
121
  }
69
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
122
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
70
123
  const lintScript = packageJson.scripts?.lint;
71
124
  if (!lintScript) {
72
125
  return;
73
126
  }
74
127
  ctx.logger.debug(`Running lint: ${lintScript}`);
75
- execSync(lintScript, { cwd: ctx.baseDir, stdio: 'pipe' });
76
- ctx.logger.debug('Lint passed');
128
+ await runAsync(lintScript, ctx.baseDir, 30_000);
129
+ ctx.logger.debug("Lint passed");
77
130
  }
78
131
  catch (err) {
79
132
  ctx.logger.warn(`Lint failed: ${err.message}`);
@@ -81,41 +134,93 @@ export class LintOnWritePlugin {
81
134
  }
82
135
  }
83
136
  async runProjectTypeCheck(ctx, result) {
84
- const tsconfigPath = join(ctx.baseDir, 'tsconfig.json');
137
+ const tsconfigPath = join(ctx.baseDir, "tsconfig.json");
85
138
  if (!existsSync(tsconfigPath)) {
86
139
  return;
87
140
  }
88
141
  const now = Date.now();
89
- if (projectTypeCheckPromise && now - projectTypeCheckTimestamp < TYPE_CHECK_DEBOUNCE_MS) {
90
- const error = await projectTypeCheckPromise;
142
+ if (this._checkPromise &&
143
+ now - this._checkTimestamp < TYPE_CHECK_DEBOUNCE_MS) {
144
+ const error = await this._checkPromise;
91
145
  if (error) {
92
146
  result.output += `\n\n[Project typecheck failed]: ${error}`;
93
147
  }
94
148
  return;
95
149
  }
96
- projectTypeCheckTimestamp = now;
97
- projectTypeCheckPromise = this.runTscCheck(ctx.baseDir);
98
- const error = await projectTypeCheckPromise;
150
+ this._checkTimestamp = now;
151
+ this._checkPromise = this.runTscCheck(ctx.baseDir);
152
+ const error = await this._checkPromise;
99
153
  if (error) {
100
154
  result.output += `\n\n[Project typecheck failed]: ${error}`;
101
155
  }
102
156
  }
103
157
  async runTscCheck(baseDir) {
104
158
  try {
105
- execSync(`npx tsc --noEmit --skipLibCheck`, { cwd: baseDir, stdio: 'pipe', timeout: 30000 });
159
+ await runAsync(`npx tsc --noEmit --skipLibCheck`, baseDir, 30000);
106
160
  return null;
107
161
  }
108
162
  catch (err) {
109
- if (err.status === 127 || err.message.includes('not found') || err.message.includes('ENOENT')) {
163
+ if (err.status === 127 ||
164
+ err.message.includes("not found") ||
165
+ err.message.includes("ENOENT")) {
110
166
  return null;
111
167
  }
112
- const stderr = err.stderr?.toString() || err.stdout?.toString() || '';
113
- if (stderr.includes('error TS')) {
114
- const firstError = stderr.split('\n').find((line) => line.includes('error TS')) || 'TypeScript type error';
168
+ const stderr = err.stderr?.toString() || err.stdout?.toString() || "";
169
+ if (stderr.includes("error TS")) {
170
+ const firstError = stderr
171
+ .split("\n")
172
+ .find((line) => line.includes("error TS")) ||
173
+ "TypeScript type error";
115
174
  return firstError.trim();
116
175
  }
117
176
  return null;
118
177
  }
119
178
  }
120
179
  }
180
+ /**
181
+ * Run a command asynchronously (non-blocking event loop) and resolve when it
182
+ * exits. Rejects with captured stderr/stdout on non-zero exit or timeout.
183
+ */
184
+ function runAsync(command, cwd, timeoutMs) {
185
+ return new Promise((resolve, reject) => {
186
+ const child = spawn(command, {
187
+ cwd,
188
+ shell: true,
189
+ windowsHide: true,
190
+ stdio: ["ignore", "pipe", "pipe"],
191
+ });
192
+ let stdout = "";
193
+ let stderr = "";
194
+ const decoder = getWinDecoder();
195
+ child.stdout?.on("data", (d) => {
196
+ stdout += decoder.decode(d, { stream: true });
197
+ });
198
+ child.stderr?.on("data", (d) => {
199
+ stderr += decoder.decode(d, { stream: true });
200
+ });
201
+ const timer = setTimeout(() => {
202
+ child.kill();
203
+ reject(new Error(`Command timed out after ${timeoutMs}ms`));
204
+ }, timeoutMs);
205
+ child.on("error", (err) => {
206
+ clearTimeout(timer);
207
+ reject(err);
208
+ });
209
+ child.on("close", (code) => {
210
+ clearTimeout(timer);
211
+ stdout += decoder.decode();
212
+ stderr += decoder.decode();
213
+ if (code === 0) {
214
+ resolve({ stdout, stderr });
215
+ }
216
+ else {
217
+ const err = new Error(`Command failed with exit code ${code}`);
218
+ err.stdout = stdout;
219
+ err.stderr = stderr;
220
+ err.status = code;
221
+ reject(err);
222
+ }
223
+ });
224
+ });
225
+ }
121
226
  export const plugin = new LintOnWritePlugin();
@@ -1,3 +1,2 @@
1
- export { runCommand, killByCallId } from "./runner";
1
+ export { registerKillable, unregisterKillable, killByCallId, } from "./runner";
2
2
  export { processRegistry } from "./registry";
3
- export { isLongRunningCommand } from "./detect";
@@ -1,6 +1,6 @@
1
- import { spawn } from "child_process";
1
+ import { spawn, spawnSync } from "child_process";
2
2
  import { platform } from "os";
3
- const MAX_LOG_LINES = 300;
3
+ const MAX_LOG_LINES = 2000;
4
4
  const MAX_KEPT_PROCESSES = 20;
5
5
  let seq = 0;
6
6
  function killTree(child) {
@@ -8,10 +8,16 @@ function killTree(child) {
8
8
  if (!pid)
9
9
  return;
10
10
  if (platform() === "win32") {
11
- spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
12
- windowsHide: true,
13
- stdio: "ignore",
14
- });
11
+ try {
12
+ spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], {
13
+ windowsHide: true,
14
+ stdio: "ignore",
15
+ timeout: 3000,
16
+ });
17
+ }
18
+ catch {
19
+ /* already dead */
20
+ }
15
21
  return;
16
22
  }
17
23
  try {
@@ -29,9 +35,10 @@ function killTree(child) {
29
35
  class ProcessRegistry {
30
36
  procs = new Map();
31
37
  children = new Map();
38
+ exitWaiters = new Map();
32
39
  /**
33
- * Start a background process. Returns immediately with the entry; output is
34
- * buffered into `entry.log` as it arrives.
40
+ * Start a command. The returned entry buffers output and reports status; a
41
+ * call may check `waitForExit(id)` to learn when (if ever) it finishes.
35
42
  */
36
43
  start(command, cwd, sessionId) {
37
44
  const id = `proc_${Date.now()}_${++seq}`;
@@ -48,46 +55,98 @@ class ProcessRegistry {
48
55
  };
49
56
  this.trimOldEntries();
50
57
  this.procs.set(id, entry);
51
- const child = spawn(command, {
52
- cwd,
53
- shell: true,
54
- windowsHide: true,
55
- detached: platform() !== "win32",
56
- stdio: ["ignore", "pipe", "pipe"],
57
- });
58
+ let child;
59
+ try {
60
+ child = spawn(command, {
61
+ cwd,
62
+ shell: true,
63
+ windowsHide: true,
64
+ detached: platform() !== "win32",
65
+ stdio: ["ignore", "pipe", "pipe"],
66
+ });
67
+ }
68
+ catch (e) {
69
+ entry.status = "killed";
70
+ entry.spawnError = e?.message || String(e);
71
+ entry.log.push(`[process error] ${entry.spawnError}`);
72
+ return entry;
73
+ }
58
74
  this.children.set(id, child);
59
75
  entry.pid = child.pid ?? 0;
60
- let partial = "";
76
+ // On Windows, children spawned via cmd.exe output in the OEM code page,
77
+ // NOT UTF-8. Detect the active code page via chcp.com; fall back to
78
+ // ibm866 (Russian CP866) when detection fails — still correct for the
79
+ // majority of users, and always better than the UTF-8 default.
80
+ const decoder = platform() === "win32"
81
+ ? (() => {
82
+ try {
83
+ const cpOut = require("child_process")
84
+ .execSync("chcp.com", {
85
+ encoding: "buffer",
86
+ timeout: 2000,
87
+ windowsHide: true,
88
+ })
89
+ .toString("latin1");
90
+ const m = cpOut.match(/(\d+)/);
91
+ if (m)
92
+ return new TextDecoder("ibm" + m[1]);
93
+ }
94
+ catch {
95
+ /* ignore */
96
+ }
97
+ return new TextDecoder("ibm866");
98
+ })()
99
+ : new TextDecoder("utf-8");
100
+ let buf = "";
101
+ const pushLine = (line) => {
102
+ if (entry.log.length >= MAX_LOG_LINES) {
103
+ entry.log.shift();
104
+ }
105
+ entry.log.push(line);
106
+ };
61
107
  const append = (chunk) => {
62
- const text = partial + chunk.toString();
63
- const lines = text.split(/\r?\n/);
64
- partial = lines.pop() ?? "";
108
+ const decoded = decoder.decode(chunk, { stream: true });
109
+ buf += decoded;
110
+ const lines = buf.split(/\r?\n/);
111
+ buf = lines.pop() ?? "";
65
112
  for (const line of lines) {
66
- if (entry.log.length >= MAX_LOG_LINES) {
67
- entry.log.shift();
68
- }
69
- entry.log.push(line);
113
+ pushLine(line);
70
114
  }
71
115
  };
116
+ const flush = () => {
117
+ buf += decoder.decode();
118
+ if (buf) {
119
+ pushLine(buf);
120
+ buf = "";
121
+ }
122
+ };
123
+ const resolveExit = () => {
124
+ this.exitWaiters.get(id)?.resolve();
125
+ };
72
126
  child.stdout?.on("data", append);
73
127
  child.stderr?.on("data", append);
74
128
  child.on("error", (err) => {
75
129
  if (entry.status === "running") {
76
130
  entry.status = "killed";
77
131
  }
132
+ entry.spawnError = err.message;
78
133
  append(Buffer.from(`[process error] ${err.message}`));
134
+ resolveExit();
79
135
  });
80
136
  child.on("close", (code) => {
137
+ flush();
81
138
  if (entry.status === "running") {
82
139
  entry.status = "exited";
83
140
  }
84
141
  entry.exitCode = code;
85
142
  this.children.delete(id);
86
- if (partial) {
87
- entry.log.push(partial);
88
- partial = "";
89
- }
143
+ resolveExit();
90
144
  });
145
+ let resolve;
146
+ const promise = new Promise((r) => {
147
+ resolve = r;
148
+ });
149
+ this.exitWaiters.set(id, { promise, resolve });
91
150
  return entry;
92
151
  }
93
152
  list(sessionId) {
@@ -128,20 +187,51 @@ class ProcessRegistry {
128
187
  }
129
188
  return targets.length;
130
189
  }
190
+ /**
191
+ * Resolve `true` when the process exits (or fails to spawn) within
192
+ * `timeoutMs`, otherwise `false` (still running → a background process).
193
+ */
194
+ waitForExit(id, timeoutMs) {
195
+ const entry = this.procs.get(id);
196
+ const waiter = this.exitWaiters.get(id);
197
+ if (!entry || entry.status !== "running" || !waiter) {
198
+ return Promise.resolve(true);
199
+ }
200
+ return new Promise((resolve) => {
201
+ const timer = setTimeout(() => resolve(false), timeoutMs);
202
+ waiter.promise.then(() => {
203
+ clearTimeout(timer);
204
+ resolve(true);
205
+ });
206
+ });
207
+ }
208
+ /**
209
+ * Remove a finished entry (foreground results do not linger). A still-running
210
+ * entry is killed first so nothing is orphaned.
211
+ */
212
+ remove(id) {
213
+ const entry = this.procs.get(id);
214
+ if (!entry)
215
+ return false;
216
+ if (entry.status === "running") {
217
+ const child = this.children.get(id);
218
+ if (child) {
219
+ killTree(child);
220
+ }
221
+ entry.status = "killed";
222
+ }
223
+ this.procs.delete(id);
224
+ this.children.delete(id);
225
+ this.exitWaiters.delete(id);
226
+ return true;
227
+ }
131
228
  trimOldEntries() {
132
229
  if (this.procs.size < MAX_KEPT_PROCESSES)
133
230
  return;
134
231
  const sorted = Array.from(this.procs.values()).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
135
232
  const toRemove = sorted.slice(0, sorted.length - MAX_KEPT_PROCESSES + 1);
136
233
  for (const entry of toRemove) {
137
- if (entry.status === "running") {
138
- const child = this.children.get(entry.id);
139
- if (child) {
140
- killTree(child);
141
- }
142
- }
143
- this.procs.delete(entry.id);
144
- this.children.delete(entry.id);
234
+ this.remove(entry.id);
145
235
  }
146
236
  }
147
237
  }
@@ -1,11 +1,13 @@
1
- import { spawn } from "child_process";
2
- import { platform } from "os";
3
1
  const activeChildren = new Map();
4
- /**
5
- * Kill a foreground command previously registered by `callId` (used by the
6
- * tool executor when the execution timeout fires). Returns true when a child
7
- * was found and killed.
8
- */
2
+ /** Register a kill callback for a tool call id (executor timeout/abort). */
3
+ export function registerKillable(callId, kill) {
4
+ activeChildren.set(callId, { kill });
5
+ }
6
+ /** Unregister a tool call's kill callback once its handler finished. */
7
+ export function unregisterKillable(callId) {
8
+ activeChildren.delete(callId);
9
+ }
10
+ /** Invoke (and drop) the kill callback registered for `callId`. */
9
11
  export function killByCallId(callId) {
10
12
  const entry = activeChildren.get(callId);
11
13
  if (!entry)
@@ -19,106 +21,3 @@ export function killByCallId(callId) {
19
21
  }
20
22
  return true;
21
23
  }
22
- function killTree(child) {
23
- const pid = child.pid;
24
- if (!pid)
25
- return;
26
- if (platform() === "win32") {
27
- spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
28
- windowsHide: true,
29
- stdio: "ignore",
30
- });
31
- return;
32
- }
33
- try {
34
- // `detached: true` makes the child a process-group leader on POSIX,
35
- // so killing -pid terminates the whole tree.
36
- process.kill(-pid, "SIGTERM");
37
- }
38
- catch {
39
- try {
40
- child.kill("SIGKILL");
41
- }
42
- catch {
43
- /* already dead */
44
- }
45
- }
46
- }
47
- /**
48
- * Run a shell command asynchronously (non-blocking event loop) and collect
49
- * stdout/stderr. The child is killed on timeout and can be killed externally
50
- * via `killByCallId` (used by the tool executor timeout).
51
- */
52
- export function runCommand(command, options = {}) {
53
- const { cwd, callId, timeoutMs = 120_000, maxBuffer = 10 * 1024 * 1024, } = options;
54
- return new Promise((resolve) => {
55
- let timedOut = false;
56
- let stdout = "";
57
- let stderr = "";
58
- const child = spawn(command, {
59
- cwd,
60
- shell: true,
61
- windowsHide: true,
62
- detached: platform() !== "win32",
63
- stdio: ["ignore", "pipe", "pipe"],
64
- });
65
- const stdoutRef = { value: stdout };
66
- const stderrRef = { value: stderr };
67
- // On Windows, child processes spawned via cmd.exe output in the OEM code
68
- // page (typically CP866 for Russian), NOT UTF-8 — even when the parent
69
- // console reports code page 65001 via `chcp`. Always decode with ibm866
70
- // so that Cyrillic and other non-ASCII text is not garbled.
71
- const decoder = platform() === "win32" ? new TextDecoder("ibm866") : null;
72
- const decodeChunk = (chunk) => decoder ? decoder.decode(chunk, { stream: true }) : chunk.toString("utf-8");
73
- child.stdout?.on("data", (chunk) => {
74
- const text = decodeChunk(chunk);
75
- const next = stdoutRef.value.length + text.length;
76
- if (next > maxBuffer) {
77
- stdoutRef.value =
78
- stdoutRef.value.slice(stdoutRef.value.length + text.length - maxBuffer) + text;
79
- }
80
- else {
81
- stdoutRef.value = stdoutRef.value + text;
82
- }
83
- });
84
- child.stderr?.on("data", (chunk) => {
85
- const text = decodeChunk(chunk);
86
- const next = stderrRef.value.length + text.length;
87
- if (next > maxBuffer) {
88
- stderrRef.value =
89
- stderrRef.value.slice(stderrRef.value.length + text.length - maxBuffer) + text;
90
- }
91
- else {
92
- stderrRef.value = stderrRef.value + text;
93
- }
94
- });
95
- const entry = {
96
- kill: () => killTree(child),
97
- };
98
- if (callId) {
99
- activeChildren.set(callId, entry);
100
- }
101
- const timer = setTimeout(() => {
102
- timedOut = true;
103
- killTree(child);
104
- }, timeoutMs);
105
- child.on("error", () => {
106
- clearTimeout(timer);
107
- if (callId)
108
- activeChildren.delete(callId);
109
- resolve({
110
- stdout: stdoutRef.value,
111
- stderr: stderrRef.value,
112
- code: null,
113
- signal: null,
114
- timedOut,
115
- });
116
- });
117
- child.on("close", (code, signal) => {
118
- clearTimeout(timer);
119
- if (callId)
120
- activeChildren.delete(callId);
121
- resolve({ stdout: stdoutRef.value, stderr: stderrRef.value, code, signal, timedOut });
122
- });
123
- });
124
- }
@@ -1,28 +1,48 @@
1
1
  import { existsSync, mkdirSync, appendFileSync } from "fs";
2
- import { resolve } from "path";
2
+ import { resolve, join } from "path";
3
3
  import { homedir } from "os";
4
4
  import { globalAuditNotifier } from "./audit-notifier";
5
+ let _globalAuditDir = resolve(homedir(), ".mma", "logs");
6
+ let _sessionAuditDir = null;
7
+ function getAuditDir() {
8
+ return _sessionAuditDir ?? _globalAuditDir;
9
+ }
5
10
  /**
6
- * Directory and file path for audit logs
11
+ * Set the session directory for audit logs. When set, all audit entries
12
+ * are written to the session-specific directory instead of the global one.
7
13
  */
8
- const AUDIT_LOG_DIR = resolve(homedir(), ".mma", "logs");
9
- const AUDIT_LOG_PATH = resolve(AUDIT_LOG_DIR, "audit.jsonl");
14
+ export function setAuditSessionDir(dir) {
15
+ _sessionAuditDir = dir;
16
+ if (!existsSync(dir)) {
17
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
18
+ }
19
+ }
10
20
  /**
11
- * Ensure audit log directory exists
21
+ * Clear the session directory — fall back to global audit dir.
12
22
  */
13
- function ensureAuditLogDir() {
14
- if (!existsSync(AUDIT_LOG_DIR)) {
15
- mkdirSync(AUDIT_LOG_DIR, { recursive: true, mode: 0o700 });
23
+ export function clearAuditSessionDir() {
24
+ _sessionAuditDir = null;
25
+ }
26
+ /**
27
+ * Set the global audit log directory (used when no session dir is active).
28
+ */
29
+ export function setGlobalAuditDir(dir) {
30
+ _globalAuditDir = dir;
31
+ if (!existsSync(dir)) {
32
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
16
33
  }
17
34
  }
18
35
  /**
19
36
  * Write an audit log entry
20
37
  */
21
38
  export function logAudit(entry) {
22
- ensureAuditLogDir();
39
+ const dir = getAuditDir();
40
+ if (!existsSync(dir)) {
41
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
42
+ }
23
43
  try {
24
44
  const logEntry = JSON.stringify(entry);
25
- appendFileSync(AUDIT_LOG_PATH, logEntry + "\n", "utf8");
45
+ appendFileSync(join(dir, "audit.jsonl"), logEntry + "\n", "utf8");
26
46
  }
27
47
  catch {
28
48
  // Silently fail if we can't write to audit log