sub-agents-mcp 0.14.0 → 0.14.2

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 (48) hide show
  1. package/README.md +67 -544
  2. package/dist/agents/AgentManager.d.ts +16 -0
  3. package/dist/agents/AgentManager.d.ts.map +1 -1
  4. package/dist/agents/AgentManager.js +68 -43
  5. package/dist/agents/AgentManager.js.map +1 -1
  6. package/dist/agents/AgentName.d.ts +15 -0
  7. package/dist/agents/AgentName.d.ts.map +1 -0
  8. package/dist/agents/AgentName.js +40 -0
  9. package/dist/agents/AgentName.js.map +1 -0
  10. package/dist/config/ServerConfig.d.ts.map +1 -1
  11. package/dist/config/ServerConfig.js +101 -70
  12. package/dist/config/ServerConfig.js.map +1 -1
  13. package/dist/execution/AgentExecutor.d.ts +27 -3
  14. package/dist/execution/AgentExecutor.d.ts.map +1 -1
  15. package/dist/execution/AgentExecutor.js +328 -197
  16. package/dist/execution/AgentExecutor.js.map +1 -1
  17. package/dist/execution/StreamProcessor.d.ts.map +1 -1
  18. package/dist/execution/StreamProcessor.js +17 -16
  19. package/dist/execution/StreamProcessor.js.map +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +8 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/resources/AgentResources.d.ts.map +1 -1
  24. package/dist/resources/AgentResources.js +2 -8
  25. package/dist/resources/AgentResources.js.map +1 -1
  26. package/dist/server/McpServer.d.ts.map +1 -1
  27. package/dist/server/McpServer.js +40 -21
  28. package/dist/server/McpServer.js.map +1 -1
  29. package/dist/session/SessionManager.d.ts +11 -1
  30. package/dist/session/SessionManager.d.ts.map +1 -1
  31. package/dist/session/SessionManager.js +118 -28
  32. package/dist/session/SessionManager.js.map +1 -1
  33. package/dist/tools/RunAgentTool.d.ts +32 -9
  34. package/dist/tools/RunAgentTool.d.ts.map +1 -1
  35. package/dist/tools/RunAgentTool.js +281 -190
  36. package/dist/tools/RunAgentTool.js.map +1 -1
  37. package/dist/types/ExecutionParams.d.ts +0 -1
  38. package/dist/types/ExecutionParams.d.ts.map +1 -1
  39. package/dist/types/SessionData.d.ts +6 -1
  40. package/dist/types/SessionData.d.ts.map +1 -1
  41. package/dist/utils/ErrorHandler.d.ts +14 -2
  42. package/dist/utils/ErrorHandler.d.ts.map +1 -1
  43. package/dist/utils/ErrorHandler.js +40 -9
  44. package/dist/utils/ErrorHandler.js.map +1 -1
  45. package/dist/utils/Logger.d.ts.map +1 -1
  46. package/dist/utils/Logger.js +3 -2
  47. package/dist/utils/Logger.js.map +1 -1
  48. package/package.json +10 -14
@@ -3,7 +3,8 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { StringDecoder } from 'node:string_decoder';
6
- import { Logger } from '../utils/Logger.js';
6
+ import { toErrorMessage } from '../utils/ErrorHandler.js';
7
+ import { isLogLevel, Logger } from '../utils/Logger.js';
7
8
  import { StreamProcessor } from './StreamProcessor.js';
8
9
  export const DEFAULT_EXECUTION_TIMEOUT = 300000; // 5 minutes
9
10
  const MAX_CAPTURED_OUTPUT_BYTES = 16 * 1024 * 1024;
@@ -31,7 +32,7 @@ export const AGENT_TYPES = [
31
32
  'command-code',
32
33
  ];
33
34
  export function isAgentType(value) {
34
- return typeof value === 'string' && AGENT_TYPES.includes(value);
35
+ return typeof value === 'string' && AGENT_TYPES.some((agentType) => agentType === value);
35
36
  }
36
37
  export const AGENT_EFFORT_SUPPORTED_TYPES = [
37
38
  'codex',
@@ -44,11 +45,11 @@ export const AGENT_EFFORT_SUPPORTED_TYPES = [
44
45
  'command-code',
45
46
  ];
46
47
  export function supportsAgentEffort(agentType) {
47
- return AGENT_EFFORT_SUPPORTED_TYPES.includes(agentType);
48
+ return AGENT_EFFORT_SUPPORTED_TYPES.some((supported) => supported === agentType);
48
49
  }
49
50
  export const AGENT_PERMISSIONS = ['read-only', 'safe-edit', 'yolo'];
50
51
  export function isAgentPermission(value) {
51
- return typeof value === 'string' && AGENT_PERMISSIONS.includes(value);
52
+ return typeof value === 'string' && AGENT_PERMISSIONS.some((permission) => permission === value);
52
53
  }
53
54
  export const DEFAULT_AGENT_PERMISSION = 'safe-edit';
54
55
  const PERMISSION_FLAGS = {
@@ -110,7 +111,6 @@ const PERMISSION_FLAGS = {
110
111
  const OPENCODE_PERMISSION_MAPPING = {
111
112
  'read-only': {
112
113
  edit: 'deny',
113
- bash: 'deny',
114
114
  task: 'deny',
115
115
  external_directory: 'deny',
116
116
  question: 'deny',
@@ -136,14 +136,258 @@ export function createExecutionConfig(agentType, overrides) {
136
136
  agentType,
137
137
  };
138
138
  }
139
+ /** Reads LOG_LEVEL from the environment, falling back to `info` when unset or invalid. */
140
+ function resolveLogLevelFromEnv() {
141
+ const value = process.env['LOG_LEVEL'];
142
+ return isLogLevel(value) ? value : 'info';
143
+ }
144
+ function errorCode(error) {
145
+ if (typeof error !== 'object' || error === null || !('code' in error)) {
146
+ return undefined;
147
+ }
148
+ return typeof error.code === 'string' ? error.code : undefined;
149
+ }
150
+ function signalNumber(signal) {
151
+ if (signal === 'SIGTERM') {
152
+ return 15;
153
+ }
154
+ if (signal === 'SIGKILL') {
155
+ return 9;
156
+ }
157
+ return 1;
158
+ }
159
+ /**
160
+ * Owns the lifecycle of one spawned agent process: output capture with a byte
161
+ * cap, incremental stream parsing, timeout-driven termination, and settlement
162
+ * into a single {@link SpawnOutcome}.
163
+ */
164
+ class SpawnSession {
165
+ childProcess;
166
+ config;
167
+ logger;
168
+ cleanup;
169
+ streamProcessor;
170
+ stdoutParts = [];
171
+ stderrParts = [];
172
+ stdoutDecoder = new StringDecoder('utf8');
173
+ stderrDecoder = new StringDecoder('utf8');
174
+ stdoutLineParts = [];
175
+ stdoutTruncated = false;
176
+ stderrTruncated = false;
177
+ capturedBytes = 0;
178
+ timedOut = false;
179
+ cancelled = false;
180
+ outputExceeded = false;
181
+ processError;
182
+ settled = false;
183
+ forceKillTimer;
184
+ executionTimeout;
185
+ constructor(childProcess, config, logger, cleanup) {
186
+ this.childProcess = childProcess;
187
+ this.config = config;
188
+ this.logger = logger;
189
+ this.cleanup = cleanup;
190
+ this.streamProcessor = new StreamProcessor(config.agentType);
191
+ }
192
+ run(cancelSignal) {
193
+ return new Promise((resolve) => {
194
+ if (cancelSignal) {
195
+ // Reuses the same graceful SIGTERM -> SIGKILL path as a timeout, so a
196
+ // cancelled request cannot leave the CLI running.
197
+ if (cancelSignal.aborted) {
198
+ queueMicrotask(() => this.cancel());
199
+ }
200
+ else {
201
+ cancelSignal.addEventListener('abort', () => this.cancel(), { once: true });
202
+ }
203
+ }
204
+ const settle = (code, signal) => {
205
+ if (this.settled) {
206
+ return;
207
+ }
208
+ this.settled = true;
209
+ this.finish(code, signal).then(resolve, (error) => {
210
+ resolve({
211
+ stdout: '',
212
+ stderr: toErrorMessage(error),
213
+ exitCode: 1,
214
+ hasResult: false,
215
+ });
216
+ });
217
+ };
218
+ this.executionTimeout = setTimeout(() => {
219
+ this.timedOut = true;
220
+ this.logger.warn('Execution timeout reached', { timeout: this.config.executionTimeout });
221
+ this.requestTermination();
222
+ }, this.config.executionTimeout);
223
+ this.childProcess.stdout?.on('data', (data) => {
224
+ this.consumeStdout(data);
225
+ });
226
+ this.childProcess.stderr?.on('data', (data) => {
227
+ this.stderrParts.push(this.captureChunk(data, this.stderrDecoder, () => {
228
+ this.stderrTruncated = true;
229
+ }));
230
+ });
231
+ this.childProcess.on('close', (code, signal) => {
232
+ settle(code, signal);
233
+ });
234
+ this.childProcess.on('error', (error) => {
235
+ this.processError = error;
236
+ settle(null);
237
+ });
238
+ });
239
+ }
240
+ /**
241
+ * Stops the agent process, whether the client cancelled the request or the
242
+ * server is shutting down. Reuses the graceful SIGTERM -> SIGKILL escalation.
243
+ */
244
+ cancel() {
245
+ if (this.settled || this.cancelled) {
246
+ return;
247
+ }
248
+ this.cancelled = true;
249
+ this.logger.info('Execution cancelled before the agent finished');
250
+ this.requestTermination();
251
+ }
252
+ clearTimers() {
253
+ if (this.executionTimeout) {
254
+ clearTimeout(this.executionTimeout);
255
+ }
256
+ if (this.forceKillTimer) {
257
+ clearTimeout(this.forceKillTimer);
258
+ }
259
+ }
260
+ requestTermination() {
261
+ this.childProcess.kill('SIGTERM');
262
+ if (this.forceKillTimer) {
263
+ return;
264
+ }
265
+ this.forceKillTimer = setTimeout(() => {
266
+ this.childProcess.kill('SIGKILL');
267
+ }, TERMINATION_GRACE_MS);
268
+ }
269
+ /**
270
+ * Copies at most the remaining byte budget out of `data`, flagging truncation
271
+ * and terminating the process once the cap is reached.
272
+ */
273
+ captureChunk(data, decoder, markTruncated) {
274
+ const remaining = this.config.maxOutputBytes - this.capturedBytes;
275
+ if (remaining <= 0) {
276
+ this.outputExceeded = true;
277
+ markTruncated();
278
+ this.requestTermination();
279
+ return '';
280
+ }
281
+ const captured = data.length <= remaining ? data : data.subarray(0, remaining);
282
+ this.capturedBytes += captured.length;
283
+ if (captured.length < data.length) {
284
+ this.outputExceeded = true;
285
+ markTruncated();
286
+ this.requestTermination();
287
+ }
288
+ return decoder.write(captured);
289
+ }
290
+ consumeStdout(data) {
291
+ const chunk = this.captureChunk(data, this.stdoutDecoder, () => {
292
+ this.stdoutTruncated = true;
293
+ });
294
+ this.stdoutParts.push(chunk);
295
+ let chunkOffset = 0;
296
+ while (chunkOffset < chunk.length) {
297
+ const newlineIndex = chunk.indexOf('\n', chunkOffset);
298
+ if (newlineIndex < 0) {
299
+ this.stdoutLineParts.push(chunk.slice(chunkOffset));
300
+ break;
301
+ }
302
+ this.stdoutLineParts.push(chunk.slice(chunkOffset, newlineIndex));
303
+ const line = this.stdoutLineParts.join('');
304
+ this.stdoutLineParts = [];
305
+ chunkOffset = newlineIndex + 1;
306
+ if (this.streamProcessor.processLine(line)) {
307
+ this.requestTermination();
308
+ break;
309
+ }
310
+ }
311
+ }
312
+ /** Flushes both decoders and parses any line left without a trailing newline. */
313
+ flushStreams() {
314
+ if (!this.stdoutTruncated) {
315
+ const tail = this.stdoutDecoder.end();
316
+ this.stdoutParts.push(tail);
317
+ this.stdoutLineParts.push(tail);
318
+ }
319
+ if (!this.stderrTruncated) {
320
+ this.stderrParts.push(this.stderrDecoder.end());
321
+ }
322
+ const trailingLine = this.stdoutLineParts.join('');
323
+ if (trailingLine.trim()) {
324
+ this.streamProcessor.processLine(trailingLine);
325
+ }
326
+ this.stdoutLineParts = [];
327
+ }
328
+ resolveExitCode(code, signal) {
329
+ if (this.outputExceeded || this.processError) {
330
+ return errorCode(this.processError) === 'ENOENT' ? 127 : 1;
331
+ }
332
+ if (this.timedOut) {
333
+ return 124;
334
+ }
335
+ if (this.cancelled) {
336
+ return 130;
337
+ }
338
+ return code ?? (signal ? 128 + signalNumber(signal) : 1);
339
+ }
340
+ collectErrors(stderr) {
341
+ const errors = [];
342
+ if (stderr) {
343
+ errors.push(stderr);
344
+ }
345
+ if (this.timedOut) {
346
+ errors.push(`Execution timeout: ${this.config.executionTimeout}ms`);
347
+ }
348
+ if (this.cancelled) {
349
+ errors.push('Execution was cancelled by the client before the agent finished.');
350
+ }
351
+ if (this.outputExceeded) {
352
+ errors.push(`Sub-agent output exceeded ${this.config.maxOutputBytes} bytes`);
353
+ }
354
+ if (this.processError && !stderr) {
355
+ errors.push(this.processError.message);
356
+ }
357
+ return errors;
358
+ }
359
+ async finish(code, signal) {
360
+ this.clearTimers();
361
+ this.flushStreams();
362
+ const stdout = this.stdoutParts.join('');
363
+ const stderr = this.stderrParts.join('');
364
+ let result = this.streamProcessor.getResult();
365
+ if (result === null) {
366
+ this.streamProcessor.processCompleteOutput(stdout);
367
+ result = this.streamProcessor.getResult();
368
+ }
369
+ await this.cleanup();
370
+ return {
371
+ stdout: result ? JSON.stringify(result) : stdout,
372
+ stderr: this.collectErrors(stderr).join('\n'),
373
+ exitCode: this.resolveExitCode(code, signal),
374
+ hasResult: result !== null,
375
+ resultJson: result !== null ? result : undefined,
376
+ };
377
+ }
378
+ }
139
379
  export class AgentExecutor {
140
380
  config;
141
381
  logger;
142
382
  constructor(config, logger) {
143
383
  this.config = config;
144
- this.logger = logger || new Logger(process.env['LOG_LEVEL'] || 'info');
384
+ this.logger = logger || new Logger(resolveLogLevelFromEnv());
145
385
  }
146
- async executeAgent(params) {
386
+ /**
387
+ * Guards against untrusted callers that bypass the declared parameter type
388
+ * (for example MCP requests deserialized as `unknown`).
389
+ */
390
+ assertExecutableParams(params) {
147
391
  if (!params?.agent || !params.prompt) {
148
392
  const error = 'Invalid execution parameters: agent and prompt are required';
149
393
  this.logger.error('Agent execution failed during validation', undefined, { error, params });
@@ -154,6 +398,17 @@ export class AgentExecutor {
154
398
  this.logger.error('Agent execution failed during validation', undefined, { error, params });
155
399
  throw new Error(error);
156
400
  }
401
+ }
402
+ /** Sessions still running, so shutdown does not orphan agent processes. */
403
+ activeSessions = new Set();
404
+ /** Terminates every agent process this executor still has running. */
405
+ terminateAll() {
406
+ for (const session of this.activeSessions) {
407
+ session.cancel();
408
+ }
409
+ }
410
+ async executeAgent(params, signal) {
411
+ this.assertExecutableParams(params);
157
412
  const startTime = Date.now();
158
413
  const requestId = this.generateRequestId();
159
414
  this.logger.info('Starting agent execution', {
@@ -161,12 +416,9 @@ export class AgentExecutor {
161
416
  agent: params.agent,
162
417
  promptLength: params.prompt.length,
163
418
  cwd: params.cwd,
164
- extraArgs: params.extra_args?.length || 0,
165
419
  });
166
420
  try {
167
- // Add minimal delay to ensure execution time is measurable
168
- await new Promise((resolve) => setTimeout(resolve, 1));
169
- const result = await this.executeWithSpawn(params);
421
+ const result = await this.executeWithSpawn(params, signal);
170
422
  const executionTime = Date.now() - startTime;
171
423
  this.logger.info('Agent execution completed', {
172
424
  requestId,
@@ -181,6 +433,7 @@ export class AgentExecutor {
181
433
  executionTime,
182
434
  ...(result.hasResult !== undefined && { hasResult: result.hasResult }),
183
435
  ...(result.resultJson !== undefined && { resultJson: result.resultJson }),
436
+ ...(result.failureReason !== undefined && { failureReason: result.failureReason }),
184
437
  };
185
438
  }
186
439
  catch (error) {
@@ -189,10 +442,6 @@ export class AgentExecutor {
189
442
  requestId,
190
443
  executionTime,
191
444
  });
192
- if (error instanceof Error &&
193
- (error.message.includes('enhance') || error.message.includes('Enhancement'))) {
194
- throw error;
195
- }
196
445
  return {
197
446
  stdout: '',
198
447
  stderr: error instanceof Error ? error.message : 'Unknown execution error',
@@ -230,8 +479,9 @@ export class AgentExecutor {
230
479
  }
231
480
  buildSettingsPathEnv() {
232
481
  const env = {};
233
- if (!this.config.agentsSettingsPath)
482
+ if (!this.config.agentsSettingsPath) {
234
483
  return env;
484
+ }
235
485
  switch (this.config.agentType) {
236
486
  case 'cursor':
237
487
  env['CURSOR_CONFIG_DIR'] = this.config.agentsSettingsPath;
@@ -332,16 +582,25 @@ export class AgentExecutor {
332
582
  if (!apiKey?.trim()) {
333
583
  throw new Error(GLM_MISSING_API_KEY_ERROR);
334
584
  }
335
- return this.buildRedirectedClaudeArgs(params, envOverrides, GLM_BASE_URL, apiKey, 'ANTHROPIC_AUTH_TOKEN');
585
+ return this.buildRedirectedClaudeArgs(params, envOverrides, {
586
+ baseUrl: GLM_BASE_URL,
587
+ apiKey,
588
+ credentialEnv: 'ANTHROPIC_AUTH_TOKEN',
589
+ });
336
590
  }
337
591
  buildKimiArgs(params, envOverrides) {
338
592
  const apiKey = this.config.kimiApiKey;
339
593
  if (!apiKey?.trim()) {
340
594
  throw new Error(KIMI_MISSING_API_KEY_ERROR);
341
595
  }
342
- return this.buildRedirectedClaudeArgs(params, envOverrides, KIMI_BASE_URL, apiKey, 'ANTHROPIC_API_KEY');
596
+ return this.buildRedirectedClaudeArgs(params, envOverrides, {
597
+ baseUrl: KIMI_BASE_URL,
598
+ apiKey,
599
+ credentialEnv: 'ANTHROPIC_API_KEY',
600
+ });
343
601
  }
344
- buildRedirectedClaudeArgs(params, envOverrides, baseUrl, apiKey, credentialEnv) {
602
+ buildRedirectedClaudeArgs(params, envOverrides, redirect) {
603
+ const { baseUrl, apiKey, credentialEnv } = redirect;
345
604
  const flags = this.invocationFlags();
346
605
  const cwd = params.cwd || process.cwd();
347
606
  const systemPrompt = `cwd: ${cwd}\n\n${params.agent}`;
@@ -460,10 +719,10 @@ export class AgentExecutor {
460
719
  await fs.promises.copyFile(authSource, authDestination);
461
720
  }
462
721
  catch (error) {
463
- const code = this.errorCode(error);
722
+ const code = errorCode(error);
464
723
  if (code !== 'ENOENT') {
465
724
  this.logger.warn('Could not copy OpenCode authentication into isolated data home', {
466
- error: error instanceof Error ? error.message : String(error),
725
+ error: toErrorMessage(error),
467
726
  });
468
727
  }
469
728
  }
@@ -483,189 +742,61 @@ export class AgentExecutor {
483
742
  throw error;
484
743
  }
485
744
  }
486
- errorCode(error) {
487
- if (typeof error !== 'object' || error === null || !('code' in error)) {
488
- return undefined;
489
- }
490
- return typeof error.code === 'string' ? error.code : undefined;
491
- }
492
- async executeWithSpawn(params) {
745
+ async executeWithSpawn(params, signal) {
493
746
  const { command, args, envOverrides } = this.buildCommandArgs(params);
494
747
  const preparedEnvironment = await this.prepareSpawnEnvironment(envOverrides);
495
- return new Promise((resolve) => {
496
- this.logger.debug('Executing with spawn', {
497
- command,
748
+ this.logger.debug('Executing with spawn', {
749
+ command,
750
+ cwd: params.cwd || process.cwd(),
751
+ });
752
+ let childProcess;
753
+ try {
754
+ childProcess = spawn(command, args, {
498
755
  cwd: params.cwd || process.cwd(),
756
+ stdio: ['ignore', 'pipe', 'pipe'],
757
+ shell: false,
758
+ env: preparedEnvironment.env,
499
759
  });
500
- let childProcess;
501
- try {
502
- childProcess = spawn(command, args, {
503
- cwd: params.cwd || process.cwd(),
504
- stdio: ['ignore', 'pipe', 'pipe'],
505
- shell: false,
506
- env: preparedEnvironment.env,
507
- });
508
- }
509
- catch (error) {
510
- void preparedEnvironment.cleanup().finally(() => {
511
- resolve({
512
- stdout: '',
513
- stderr: error instanceof Error ? error.message : String(error),
514
- exitCode: this.errorCode(error) === 'ENOENT' ? 127 : 1,
515
- hasResult: false,
516
- });
517
- });
518
- return;
760
+ }
761
+ catch (error) {
762
+ await this.cleanupQuietly(preparedEnvironment.cleanup);
763
+ if (errorCode(error) === 'E2BIG') {
764
+ const promptBytes = Buffer.byteLength(params.prompt, 'utf8');
765
+ return {
766
+ stdout: '',
767
+ stderr: `The prompt is too large to pass to the "${command}" CLI: ` +
768
+ `${promptBytes} bytes exceeds this operating system's argument limit.`,
769
+ exitCode: 1,
770
+ hasResult: false,
771
+ failureReason: 'argv_too_long',
772
+ };
519
773
  }
520
- const streamProcessor = new StreamProcessor(this.config.agentType);
521
- const stdoutParts = [];
522
- const stderrParts = [];
523
- let stdoutLineParts = [];
524
- const stdoutDecoder = new StringDecoder('utf8');
525
- const stderrDecoder = new StringDecoder('utf8');
526
- let stdoutTruncated = false;
527
- let stderrTruncated = false;
528
- let capturedBytes = 0;
529
- let timedOut = false;
530
- let outputExceeded = false;
531
- let processError;
532
- let settled = false;
533
- let forceKillTimer;
534
- const executionTimeout = setTimeout(() => {
535
- timedOut = true;
536
- this.logger.warn('Execution timeout reached', {
537
- timeout: this.config.executionTimeout,
538
- });
539
- requestTermination();
540
- }, this.config.executionTimeout);
541
- const clearTimers = () => {
542
- clearTimeout(executionTimeout);
543
- if (forceKillTimer)
544
- clearTimeout(forceKillTimer);
545
- };
546
- const finish = async (code, signal) => {
547
- if (settled)
548
- return;
549
- settled = true;
550
- clearTimers();
551
- if (!stdoutTruncated) {
552
- const tail = stdoutDecoder.end();
553
- stdoutParts.push(tail);
554
- stdoutLineParts.push(tail);
555
- }
556
- if (!stderrTruncated) {
557
- stderrParts.push(stderrDecoder.end());
558
- }
559
- const trailingLine = stdoutLineParts.join('');
560
- if (trailingLine.trim()) {
561
- streamProcessor.processLine(trailingLine);
562
- }
563
- stdoutLineParts = [];
564
- const stdout = stdoutParts.join('');
565
- const stderr = stderrParts.join('');
566
- let result = streamProcessor.getResult();
567
- if (result === null) {
568
- streamProcessor.processCompleteOutput(stdout);
569
- result = streamProcessor.getResult();
570
- }
571
- let exitCode = code ?? (signal ? 128 + this.signalNumber(signal) : 1);
572
- if (timedOut)
573
- exitCode = 124;
574
- if (outputExceeded || processError)
575
- exitCode = this.errorCode(processError) === 'ENOENT' ? 127 : 1;
576
- const errors = [];
577
- if (stderr)
578
- errors.push(stderr);
579
- if (timedOut)
580
- errors.push(`Execution timeout: ${this.config.executionTimeout}ms`);
581
- if (outputExceeded) {
582
- errors.push(`Sub-agent output exceeded ${this.config.maxOutputBytes} bytes`);
583
- }
584
- if (processError && !stderr)
585
- errors.push(processError.message);
586
- try {
587
- await preparedEnvironment.cleanup();
588
- }
589
- catch (error) {
590
- this.logger.warn('Failed to clean up per-run environment', {
591
- error: error instanceof Error ? error.message : String(error),
592
- });
593
- }
594
- resolve({
595
- stdout: result ? JSON.stringify(result) : stdout,
596
- stderr: errors.join('\n'),
597
- exitCode,
598
- hasResult: result !== null,
599
- resultJson: result !== null ? result : undefined,
600
- });
601
- };
602
- const requestTermination = () => {
603
- childProcess.kill('SIGTERM');
604
- if (forceKillTimer)
605
- return;
606
- forceKillTimer = setTimeout(() => {
607
- childProcess.kill('SIGKILL');
608
- }, TERMINATION_GRACE_MS);
609
- };
610
- const captureChunk = (data, decoder, markTruncated) => {
611
- const remaining = this.config.maxOutputBytes - capturedBytes;
612
- if (remaining <= 0) {
613
- outputExceeded = true;
614
- markTruncated();
615
- requestTermination();
616
- return '';
617
- }
618
- const captured = data.length <= remaining ? data : data.subarray(0, remaining);
619
- capturedBytes += captured.length;
620
- if (captured.length < data.length) {
621
- outputExceeded = true;
622
- markTruncated();
623
- requestTermination();
624
- }
625
- return decoder.write(captured);
774
+ return {
775
+ stdout: '',
776
+ stderr: toErrorMessage(error),
777
+ exitCode: errorCode(error) === 'ENOENT' ? 127 : 1,
778
+ hasResult: false,
626
779
  };
627
- childProcess.stdout?.on('data', (data) => {
628
- const chunk = captureChunk(data, stdoutDecoder, () => {
629
- stdoutTruncated = true;
630
- });
631
- stdoutParts.push(chunk);
632
- let chunkOffset = 0;
633
- while (chunkOffset < chunk.length) {
634
- const newlineIndex = chunk.indexOf('\n', chunkOffset);
635
- if (newlineIndex < 0) {
636
- stdoutLineParts.push(chunk.slice(chunkOffset));
637
- break;
638
- }
639
- stdoutLineParts.push(chunk.slice(chunkOffset, newlineIndex));
640
- const line = stdoutLineParts.join('');
641
- stdoutLineParts = [];
642
- chunkOffset = newlineIndex + 1;
643
- if (streamProcessor.processLine(line)) {
644
- requestTermination();
645
- break;
646
- }
647
- }
648
- });
649
- childProcess.stderr?.on('data', (data) => {
650
- stderrParts.push(captureChunk(data, stderrDecoder, () => {
651
- stderrTruncated = true;
652
- }));
653
- });
654
- childProcess.on('close', (code, signal) => {
655
- void finish(code, signal);
656
- });
657
- childProcess.on('error', (error) => {
658
- processError = error;
659
- void finish(null);
660
- });
661
- });
780
+ }
781
+ const session = new SpawnSession(childProcess, this.config, this.logger, () => this.cleanupQuietly(preparedEnvironment.cleanup));
782
+ this.activeSessions.add(session);
783
+ try {
784
+ return await session.run(signal);
785
+ }
786
+ finally {
787
+ this.activeSessions.delete(session);
788
+ }
662
789
  }
663
- signalNumber(signal) {
664
- if (signal === 'SIGTERM')
665
- return 15;
666
- if (signal === 'SIGKILL')
667
- return 9;
668
- return 1;
790
+ /** Runs a cleanup callback, logging rather than propagating its failures. */
791
+ async cleanupQuietly(cleanup) {
792
+ try {
793
+ await cleanup();
794
+ }
795
+ catch (error) {
796
+ this.logger.warn('Failed to clean up per-run environment', {
797
+ error: toErrorMessage(error),
798
+ });
799
+ }
669
800
  }
670
801
  generateRequestId() {
671
802
  return `req_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;