@mako10k/shell-server 0.2.4 → 0.3.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 (55) hide show
  1. package/README.md +48 -1
  2. package/dist/backoffice/index.js +1 -1
  3. package/dist/backoffice/index.js.map +1 -1
  4. package/dist/cli.js +25 -4
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/process-manager.d.ts +4 -1
  7. package/dist/core/process-manager.d.ts.map +1 -1
  8. package/dist/core/process-manager.js +299 -186
  9. package/dist/core/process-manager.js.map +1 -1
  10. package/dist/core/server-manager.d.ts +1 -0
  11. package/dist/core/server-manager.d.ts.map +1 -1
  12. package/dist/core/server-manager.js +26 -0
  13. package/dist/core/server-manager.js.map +1 -1
  14. package/dist/daemon/server.d.ts.map +1 -1
  15. package/dist/daemon/server.js +165 -1
  16. package/dist/daemon/server.js.map +1 -1
  17. package/dist/runtime/tool-runtime.d.ts.map +1 -1
  18. package/dist/runtime/tool-runtime.js +9 -2
  19. package/dist/runtime/tool-runtime.js.map +1 -1
  20. package/dist/security/chat-completion-adapter.d.ts +2 -0
  21. package/dist/security/chat-completion-adapter.d.ts.map +1 -1
  22. package/dist/security/chat-completion-adapter.js +121 -4
  23. package/dist/security/chat-completion-adapter.js.map +1 -1
  24. package/dist/security/enhanced-evaluator.d.ts +2 -0
  25. package/dist/security/enhanced-evaluator.d.ts.map +1 -1
  26. package/dist/security/enhanced-evaluator.js +177 -8
  27. package/dist/security/enhanced-evaluator.js.map +1 -1
  28. package/dist/security/evaluator-types.d.ts +2 -2
  29. package/dist/security/kfence-fastpath.d.ts +15 -0
  30. package/dist/security/kfence-fastpath.d.ts.map +1 -0
  31. package/dist/security/kfence-fastpath.js +20 -0
  32. package/dist/security/kfence-fastpath.js.map +1 -0
  33. package/dist/security/manager.d.ts +4 -0
  34. package/dist/security/manager.d.ts.map +1 -1
  35. package/dist/security/manager.js +6 -0
  36. package/dist/security/manager.js.map +1 -1
  37. package/dist/security/security-llm-prompt-generator.js +1 -1
  38. package/dist/security/security-llm-prompt-generator.js.map +1 -1
  39. package/dist/tools/shell-tools.d.ts.map +1 -1
  40. package/dist/tools/shell-tools.js +32 -14
  41. package/dist/tools/shell-tools.js.map +1 -1
  42. package/dist/types/enhanced-security.d.ts +6 -6
  43. package/dist/types/index.d.ts +18 -5
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/types/index.js +16 -13
  46. package/dist/types/index.js.map +1 -1
  47. package/dist/types/schemas.d.ts +39 -58
  48. package/dist/types/schemas.d.ts.map +1 -1
  49. package/dist/types/schemas.js +22 -27
  50. package/dist/types/schemas.js.map +1 -1
  51. package/dist/utils/errors.d.ts +1 -1
  52. package/dist/utils/errors.d.ts.map +1 -1
  53. package/dist/utils/errors.js +2 -2
  54. package/dist/utils/errors.js.map +1 -1
  55. package/package.json +1 -1
@@ -22,8 +22,68 @@ export class ProcessManager {
22
22
  fileStorageSubscriber;
23
23
  realtimeStreamSubscriber;
24
24
  enableStreaming = false; // Feature Flag
25
- constructor(maxConcurrentProcesses = 50, outputDir = '/tmp/mcp-shell-outputs', fileManager) {
26
- this.maxConcurrentProcesses = maxConcurrentProcesses;
25
+ buildConcurrencyStopCandidates(limit = 5) {
26
+ const now = Date.now();
27
+ return Array.from(this.executions.values())
28
+ .filter((exec) => exec.status === 'running')
29
+ .sort((a, b) => {
30
+ const aStart = a.started_at ? new Date(a.started_at).getTime() : Number.MAX_SAFE_INTEGER;
31
+ const bStart = b.started_at ? new Date(b.started_at).getTime() : Number.MAX_SAFE_INTEGER;
32
+ return aStart - bStart;
33
+ })
34
+ .slice(0, limit)
35
+ .map((exec) => {
36
+ const startedAtMs = exec.started_at ? new Date(exec.started_at).getTime() : undefined;
37
+ const runtimeSeconds = startedAtMs && Number.isFinite(startedAtMs)
38
+ ? Math.max(0, Math.floor((now - startedAtMs) / 1000))
39
+ : undefined;
40
+ return {
41
+ execution_id: exec.execution_id,
42
+ ...(exec.process_id !== undefined ? { process_id: exec.process_id } : {}),
43
+ command: exec.command.length > 120 ? `${exec.command.slice(0, 117)}...` : exec.command,
44
+ ...(runtimeSeconds !== undefined ? { runtime_seconds: runtimeSeconds } : {}),
45
+ status: exec.status,
46
+ suggested_action: {
47
+ tool: 'process_kill',
48
+ parameters: {
49
+ ...(exec.process_id !== undefined ? { process_id: exec.process_id } : {}),
50
+ signal: 'TERM',
51
+ force: false,
52
+ },
53
+ },
54
+ };
55
+ });
56
+ }
57
+ getRunningExecutionCount() {
58
+ return Array.from(this.executions.values()).filter((exec) => exec.status === 'running').length;
59
+ }
60
+ async waitForExecutionSlot(maxWaitMs, pollIntervalMs = 100) {
61
+ if (this.getRunningExecutionCount() < this.maxConcurrentProcesses) {
62
+ return true;
63
+ }
64
+ if (maxWaitMs <= 0) {
65
+ return false;
66
+ }
67
+ const deadline = Date.now() + maxWaitMs;
68
+ while (Date.now() < deadline) {
69
+ await new Promise((resolve) => {
70
+ setTimeout(resolve, Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
71
+ });
72
+ if (this.getRunningExecutionCount() < this.maxConcurrentProcesses) {
73
+ return true;
74
+ }
75
+ }
76
+ return this.getRunningExecutionCount() < this.maxConcurrentProcesses;
77
+ }
78
+ constructor(maxConcurrentProcesses = 8, outputDir = '/tmp/mcp-shell-outputs', fileManager) {
79
+ const envMaxConcurrentRaw = process.env['SHELL_SERVER_MAX_CONCURRENT_PROCESSES'];
80
+ const envMaxConcurrentParsed = envMaxConcurrentRaw
81
+ ? Number.parseInt(envMaxConcurrentRaw, 10)
82
+ : NaN;
83
+ const resolvedMaxConcurrent = Number.isFinite(envMaxConcurrentParsed) && envMaxConcurrentParsed > 0
84
+ ? envMaxConcurrentParsed
85
+ : maxConcurrentProcesses;
86
+ this.maxConcurrentProcesses = resolvedMaxConcurrent;
27
87
  this.outputDir = outputDir;
28
88
  this.fileManager = fileManager;
29
89
  this.defaultWorkingDirectory = process.env['SHELL_SERVER_DEFAULT_WORKDIR'] || process.cwd();
@@ -111,41 +171,70 @@ export class ProcessManager {
111
171
  await ensureDirectory(this.outputDir);
112
172
  }
113
173
  async executeCommand(options) {
114
- // Check concurrent execution limit
115
- const runningProcesses = Array.from(this.executions.values()).filter((exec) => exec.status === 'running').length;
174
+ let effectiveOptions = { ...options };
175
+ // For adaptive mode, when all slots are occupied, wait for a free slot
176
+ // using the same foreground wait budget requested by the caller.
177
+ const runningProcesses = this.getRunningExecutionCount();
116
178
  if (runningProcesses >= this.maxConcurrentProcesses) {
117
- throw new ResourceLimitError('concurrent processes', this.maxConcurrentProcesses);
179
+ const queueWaitBudgetSeconds = effectiveOptions.executionMode === 'adaptive'
180
+ ? (effectiveOptions.foregroundTimeoutSeconds ?? 10)
181
+ : 0;
182
+ const queueWaitBudgetMs = Math.max(0, Math.floor(queueWaitBudgetSeconds * 1000));
183
+ const waitStartedAt = Date.now();
184
+ const acquired = await this.waitForExecutionSlot(queueWaitBudgetMs);
185
+ const waitedMs = Date.now() - waitStartedAt;
186
+ if (!acquired) {
187
+ const currentRunning = this.getRunningExecutionCount();
188
+ const stopCandidates = this.buildConcurrencyStopCandidates();
189
+ throw new ResourceLimitError('concurrent processes', this.maxConcurrentProcesses, undefined, {
190
+ code: 'CONCURRENCY_LIMIT_EXCEEDED',
191
+ reason: `Concurrent execution limit reached (${currentRunning}/${this.maxConcurrentProcesses}) and no slot became available within ${Math.floor(waitedMs / 1000)}s queue wait budget.`,
192
+ running_count: currentRunning,
193
+ limit: this.maxConcurrentProcesses,
194
+ queue_wait_budget_seconds: queueWaitBudgetSeconds,
195
+ waited_seconds: Math.floor(waitedMs / 1000),
196
+ stop_candidates: stopCandidates,
197
+ next_steps: [
198
+ 'Call process_list with status_filter="running" to inspect active executions.',
199
+ 'Stop one or more long-running commands via process_kill, then retry shell_execute.',
200
+ ],
201
+ });
202
+ }
203
+ if (effectiveOptions.executionMode === 'adaptive') {
204
+ const originalForegroundWaitSeconds = effectiveOptions.foregroundTimeoutSeconds ?? 10;
205
+ const remainingForegroundWaitSeconds = Math.max(1, Math.floor((originalForegroundWaitSeconds * 1000 - waitedMs) / 1000));
206
+ effectiveOptions = {
207
+ ...effectiveOptions,
208
+ foregroundTimeoutSeconds: remainingForegroundWaitSeconds,
209
+ };
210
+ }
118
211
  }
119
212
  // Prepare input data when input_output_id is specified
120
- let resolvedInputData = options.inputData;
213
+ let resolvedInputData = effectiveOptions.inputData;
121
214
  let inputStream = undefined;
122
- if (options.inputOutputId) {
215
+ if (effectiveOptions.inputOutputId) {
123
216
  if (!this.fileManager) {
124
217
  throw new ExecutionError('FileManager is not available for input_output_id processing', {
125
- inputOutputId: options.inputOutputId,
218
+ inputOutputId: effectiveOptions.inputOutputId,
126
219
  });
127
220
  }
128
- // Identify execution ID from output_id
129
- const sourceExecutionId = this.findExecutionIdByOutputId(options.inputOutputId);
221
+ const sourceExecutionId = this.findExecutionIdByOutputId(effectiveOptions.inputOutputId);
130
222
  if (sourceExecutionId && this.realtimeStreamSubscriber) {
131
- // For active processes: use StreamingPipelineReader
132
223
  const streamState = this.realtimeStreamSubscriber.getStreamState(sourceExecutionId);
133
224
  if (streamState && streamState.isActive) {
134
225
  console.error(`ProcessManager: Using streaming pipeline for active process ${sourceExecutionId}`);
135
- inputStream = new StreamingPipelineReader(this.fileManager, this.realtimeStreamSubscriber, options.inputOutputId, sourceExecutionId);
226
+ inputStream = new StreamingPipelineReader(this.fileManager, this.realtimeStreamSubscriber, effectiveOptions.inputOutputId, sourceExecutionId);
136
227
  }
137
228
  }
138
- // If not active (or on failure), fall back to traditional file read
139
229
  if (!inputStream) {
140
230
  try {
141
- console.error(`ProcessManager: Using traditional file read for ${options.inputOutputId}`);
142
- const result = await this.fileManager.readFile(options.inputOutputId, 0, 100 * 1024 * 1024, // read up to 100MB
143
- 'utf-8');
231
+ console.error(`ProcessManager: Using traditional file read for ${effectiveOptions.inputOutputId}`);
232
+ const result = await this.fileManager.readFile(effectiveOptions.inputOutputId, 0, 100 * 1024 * 1024, 'utf-8');
144
233
  resolvedInputData = result.content;
145
234
  }
146
235
  catch (error) {
147
- throw new ExecutionError(`Failed to read input from output_id: ${options.inputOutputId}`, {
148
- inputOutputId: options.inputOutputId,
236
+ throw new ExecutionError(`Failed to read input from output_id: ${effectiveOptions.inputOutputId}`, {
237
+ inputOutputId: effectiveOptions.inputOutputId,
149
238
  originalError: String(error),
150
239
  });
151
240
  }
@@ -153,11 +242,10 @@ export class ProcessManager {
153
242
  }
154
243
  const executionId = generateId();
155
244
  const startTime = getCurrentTimestamp();
156
- // Initialize execution info
157
- const resolvedWorkingDirectory = this.resolveWorkingDirectory(options.workingDirectory);
245
+ const resolvedWorkingDirectory = this.resolveWorkingDirectory(effectiveOptions.workingDirectory);
158
246
  const executionInfo = {
159
247
  execution_id: executionId,
160
- command: options.command,
248
+ command: effectiveOptions.command,
161
249
  status: 'running',
162
250
  working_directory: resolvedWorkingDirectory,
163
251
  default_working_directory: this.defaultWorkingDirectory,
@@ -165,30 +253,27 @@ export class ProcessManager {
165
253
  created_at: startTime,
166
254
  started_at: startTime,
167
255
  };
168
- if (options.environmentVariables) {
169
- executionInfo.environment_variables = options.environmentVariables;
256
+ if (effectiveOptions.environmentVariables) {
257
+ executionInfo.environment_variables = effectiveOptions.environmentVariables;
170
258
  }
171
259
  this.executions.set(executionId, executionInfo);
172
- // If new terminal creation is requested
173
- if (options.createTerminal && this.terminalManager) {
260
+ if (effectiveOptions.createTerminal && this.terminalManager) {
174
261
  try {
175
262
  const terminalOptions = {
176
263
  sessionName: `exec-${executionId}`,
177
- shellType: options.terminalShell || 'bash',
178
- dimensions: options.terminalDimensions || { width: 80, height: 24 },
264
+ shellType: effectiveOptions.terminalShell || 'bash',
265
+ dimensions: effectiveOptions.terminalDimensions || { width: 80, height: 24 },
179
266
  autoSaveHistory: true,
180
267
  };
181
- if (options.workingDirectory) {
182
- terminalOptions.workingDirectory = options.workingDirectory;
268
+ if (effectiveOptions.workingDirectory) {
269
+ terminalOptions.workingDirectory = effectiveOptions.workingDirectory;
183
270
  }
184
- if (options.environmentVariables) {
185
- terminalOptions.environmentVariables = options.environmentVariables;
271
+ if (effectiveOptions.environmentVariables) {
272
+ terminalOptions.environmentVariables = effectiveOptions.environmentVariables;
186
273
  }
187
274
  const terminalInfo = await this.terminalManager.createTerminal(terminalOptions);
188
275
  executionInfo.terminal_id = terminalInfo.terminal_id;
189
- // Send command to terminal
190
- this.terminalManager.sendInput(terminalInfo.terminal_id, options.command, true);
191
- // Update execution info
276
+ this.terminalManager.sendInput(terminalInfo.terminal_id, effectiveOptions.command, true);
192
277
  executionInfo.status = 'completed';
193
278
  executionInfo.completed_at = getCurrentTimestamp();
194
279
  this.executions.set(executionId, executionInfo);
@@ -204,17 +289,15 @@ export class ProcessManager {
204
289
  }
205
290
  }
206
291
  try {
207
- // Prepare execution options
208
- const { inputOutputId: _inputOutputId, ...baseOptions } = options;
292
+ const { inputOutputId: _inputOutputId, ...baseOptions } = effectiveOptions;
209
293
  const updatedOptions = {
210
294
  ...baseOptions,
211
295
  ...(resolvedInputData !== undefined && { inputData: resolvedInputData }),
212
296
  };
213
- // Special handling when StreamingPipelineReader exists
214
297
  if (inputStream) {
215
298
  return await this.executeCommandWithInputStream(executionId, updatedOptions, inputStream);
216
299
  }
217
- switch (options.executionMode) {
300
+ switch (effectiveOptions.executionMode) {
218
301
  case 'foreground':
219
302
  return await this.executeForegroundCommand(executionId, updatedOptions);
220
303
  case 'adaptive':
@@ -224,11 +307,10 @@ export class ProcessManager {
224
307
  case 'detached':
225
308
  return await this.executeDetachedCommand(executionId, updatedOptions);
226
309
  default:
227
- throw new ExecutionError('Unsupported execution mode', { mode: options.executionMode });
310
+ throw new ExecutionError('Unsupported execution mode', { mode: effectiveOptions.executionMode });
228
311
  }
229
312
  }
230
313
  catch (error) {
231
- // Update execution info on error
232
314
  const updatedInfo = this.executions.get(executionId);
233
315
  if (updatedInfo) {
234
316
  updatedInfo.status = 'failed';
@@ -347,17 +429,21 @@ export class ProcessManager {
347
429
  reject(new ExecutionError(`Process error: ${error.message}`, { originalError: String(error) }));
348
430
  });
349
431
  // Timeout handling
350
- const timeout = setTimeout(() => {
351
- console.error(`Process timeout for ${executionId}`);
352
- child.kill('SIGTERM');
353
- setTimeout(() => {
354
- if (!child.killed) {
355
- child.kill('SIGKILL');
356
- }
357
- }, 5000);
358
- }, options.timeoutSeconds * 1000);
432
+ const timeout = options.timeoutSeconds !== undefined
433
+ ? setTimeout(() => {
434
+ console.error(`Process timeout for ${executionId}`);
435
+ child.kill('SIGTERM');
436
+ setTimeout(() => {
437
+ if (!child.killed) {
438
+ child.kill('SIGKILL');
439
+ }
440
+ }, 5000);
441
+ }, options.timeoutSeconds * 1000)
442
+ : undefined;
359
443
  child.on('close', () => {
360
- clearTimeout(timeout);
444
+ if (timeout) {
445
+ clearTimeout(timeout);
446
+ }
361
447
  });
362
448
  });
363
449
  }
@@ -379,46 +465,48 @@ export class ProcessManager {
379
465
  this.processes.set(childProcess.pid, childProcess);
380
466
  }
381
467
  // Set timeout
382
- const timeout = setTimeout(async () => {
383
- childProcess.kill('SIGTERM');
384
- setTimeout(() => {
385
- if (!childProcess.killed) {
386
- childProcess.kill('SIGKILL');
387
- }
388
- }, 5000);
389
- const executionTime = Date.now() - startTime;
390
- const executionInfo = this.executions.get(executionId);
391
- if (executionInfo) {
392
- executionInfo.status = 'timeout';
393
- executionInfo.stdout = sanitizeString(stdout);
394
- executionInfo.stderr = sanitizeString(stderr);
395
- executionInfo.completed_at = getCurrentTimestamp();
396
- executionInfo.execution_time_ms = executionTime;
397
- if (childProcess.pid !== undefined) {
398
- executionInfo.process_id = childProcess.pid;
399
- }
400
- // Save output to FileManager (regardless of size)
401
- let outputFileId;
402
- try {
403
- outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
404
- executionInfo.output_id = outputFileId;
405
- }
406
- catch (error) {
407
- // Record file-save failures as critical errors and include them in execution info
408
- console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
409
- executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
410
- }
411
- // Set detailed output status
412
- this.setOutputStatus(executionInfo, outputTruncated, 'timeout', outputFileId);
413
- this.executions.set(executionId, executionInfo);
414
- // Return partial result when return_partial_on_timeout is true
415
- if (options.returnPartialOnTimeout) {
416
- resolve(executionInfo);
417
- return;
468
+ const timeout = options.timeoutSeconds !== undefined
469
+ ? setTimeout(async () => {
470
+ childProcess.kill('SIGTERM');
471
+ setTimeout(() => {
472
+ if (!childProcess.killed) {
473
+ childProcess.kill('SIGKILL');
474
+ }
475
+ }, 5000);
476
+ const executionTime = Date.now() - startTime;
477
+ const executionInfo = this.executions.get(executionId);
478
+ if (executionInfo) {
479
+ executionInfo.status = 'timeout';
480
+ executionInfo.stdout = sanitizeString(stdout);
481
+ executionInfo.stderr = sanitizeString(stderr);
482
+ executionInfo.completed_at = getCurrentTimestamp();
483
+ executionInfo.execution_time_ms = executionTime;
484
+ if (childProcess.pid !== undefined) {
485
+ executionInfo.process_id = childProcess.pid;
486
+ }
487
+ // Save output to FileManager (regardless of size)
488
+ let outputFileId;
489
+ try {
490
+ outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
491
+ executionInfo.output_id = outputFileId;
492
+ }
493
+ catch (error) {
494
+ // Record file-save failures as critical errors and include them in execution info
495
+ console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
496
+ executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
497
+ }
498
+ // Set detailed output status
499
+ this.setOutputStatus(executionInfo, outputTruncated, 'timeout', outputFileId);
500
+ this.executions.set(executionId, executionInfo);
501
+ // Return partial result when return_partial_on_timeout is true
502
+ if (options.returnPartialOnTimeout) {
503
+ resolve(executionInfo);
504
+ return;
505
+ }
418
506
  }
419
- }
420
- reject(new TimeoutError(options.timeoutSeconds));
421
- }, options.timeoutSeconds * 1000);
507
+ reject(new TimeoutError(options.timeoutSeconds ?? 0));
508
+ }, options.timeoutSeconds * 1000)
509
+ : undefined;
422
510
  // Send stdin
423
511
  if (options.inputData) {
424
512
  childProcess.stdin?.write(options.inputData);
@@ -453,7 +541,9 @@ export class ProcessManager {
453
541
  }
454
542
  // Handle process close
455
543
  childProcess.on('close', async (code) => {
456
- clearTimeout(timeout);
544
+ if (timeout) {
545
+ clearTimeout(timeout);
546
+ }
457
547
  if (childProcess.pid) {
458
548
  this.processes.delete(childProcess.pid);
459
549
  }
@@ -542,39 +632,41 @@ export class ProcessManager {
542
632
  }
543
633
  }, foregroundTimeout * 1000);
544
634
  // Set final timeout
545
- const finalTimeoutHandle = setTimeout(async () => {
546
- childProcess.kill('SIGTERM');
547
- setTimeout(() => {
548
- if (!childProcess.killed) {
549
- childProcess.kill('SIGKILL');
550
- }
551
- }, 5000);
552
- const executionInfo = this.executions.get(executionId);
553
- if (executionInfo) {
554
- executionInfo.status = 'timeout';
555
- executionInfo.stdout = sanitizeString(stdout);
556
- executionInfo.stderr = sanitizeString(stderr);
557
- executionInfo.output_truncated = outputTruncated;
558
- executionInfo.completed_at = getCurrentTimestamp();
559
- executionInfo.execution_time_ms = Date.now() - startTime;
560
- // Save output to FileManager
561
- try {
562
- const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
563
- executionInfo.output_id = outputFileId;
564
- }
565
- catch (error) {
566
- // Record file-save failures as critical errors and include them in execution info
567
- console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
568
- executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
569
- }
570
- this.executions.set(executionId, executionInfo);
571
- if (returnPartialOnTimeout) {
572
- resolve(executionInfo);
573
- return;
635
+ const finalTimeoutHandle = options.timeoutSeconds !== undefined
636
+ ? setTimeout(async () => {
637
+ childProcess.kill('SIGTERM');
638
+ setTimeout(() => {
639
+ if (!childProcess.killed) {
640
+ childProcess.kill('SIGKILL');
641
+ }
642
+ }, 5000);
643
+ const executionInfo = this.executions.get(executionId);
644
+ if (executionInfo) {
645
+ executionInfo.status = 'timeout';
646
+ executionInfo.stdout = sanitizeString(stdout);
647
+ executionInfo.stderr = sanitizeString(stderr);
648
+ executionInfo.output_truncated = outputTruncated;
649
+ executionInfo.completed_at = getCurrentTimestamp();
650
+ executionInfo.execution_time_ms = Date.now() - startTime;
651
+ // Save output to FileManager
652
+ try {
653
+ const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
654
+ executionInfo.output_id = outputFileId;
655
+ }
656
+ catch (error) {
657
+ // Record file-save failures as critical errors and include them in execution info
658
+ console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
659
+ executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
660
+ }
661
+ this.executions.set(executionId, executionInfo);
662
+ if (returnPartialOnTimeout) {
663
+ resolve(executionInfo);
664
+ return;
665
+ }
574
666
  }
575
- }
576
- reject(new TimeoutError(options.timeoutSeconds));
577
- }, options.timeoutSeconds * 1000);
667
+ reject(new TimeoutError(options.timeoutSeconds ?? 0));
668
+ }, options.timeoutSeconds * 1000)
669
+ : undefined;
578
670
  // Function to transition to background mode
579
671
  const transitionToBackground = async () => {
580
672
  clearTimeout(foregroundTimeoutHandle);
@@ -608,9 +700,14 @@ export class ProcessManager {
608
700
  this.setOutputStatus(executionInfo, outputTruncated, 'background_transition', outputFileId);
609
701
  this.executions.set(executionId, executionInfo);
610
702
  // Configure continued background handling (adaptive mode only)
703
+ const remainingTimeoutSeconds = options.timeoutSeconds !== undefined
704
+ ? Math.max(1, options.timeoutSeconds - Math.floor((Date.now() - startTime) / 1000))
705
+ : undefined;
611
706
  this.handleAdaptiveBackgroundTransition(executionId, childProcess, {
612
707
  ...options,
613
- timeoutSeconds: Math.max(1, options.timeoutSeconds - Math.floor((Date.now() - startTime) / 1000)),
708
+ ...(remainingTimeoutSeconds !== undefined
709
+ ? { timeoutSeconds: remainingTimeoutSeconds }
710
+ : {}),
614
711
  });
615
712
  resolve(executionInfo);
616
713
  }
@@ -660,7 +757,9 @@ export class ProcessManager {
660
757
  // Handle process close
661
758
  childProcess.on('close', async (code) => {
662
759
  clearTimeout(foregroundTimeoutHandle);
663
- clearTimeout(finalTimeoutHandle);
760
+ if (finalTimeoutHandle) {
761
+ clearTimeout(finalTimeoutHandle);
762
+ }
664
763
  if (childProcess.pid) {
665
764
  this.processes.delete(childProcess.pid);
666
765
  }
@@ -697,7 +796,9 @@ export class ProcessManager {
697
796
  // Error handling
698
797
  childProcess.on('error', (error) => {
699
798
  clearTimeout(foregroundTimeoutHandle);
700
- clearTimeout(finalTimeoutHandle);
799
+ if (finalTimeoutHandle) {
800
+ clearTimeout(finalTimeoutHandle);
801
+ }
701
802
  if (childProcess.pid) {
702
803
  this.processes.delete(childProcess.pid);
703
804
  }
@@ -747,52 +848,54 @@ export class ProcessManager {
747
848
  let stdout = '';
748
849
  let stderr = '';
749
850
  // Set timeout (for background processes)
750
- const timeout = setTimeout(async () => {
751
- childProcess.kill('SIGTERM');
752
- setTimeout(() => {
753
- if (!childProcess.killed) {
754
- childProcess.kill('SIGKILL');
755
- }
756
- }, 5000);
757
- const executionInfo = this.executions.get(executionId);
758
- if (executionInfo) {
759
- executionInfo.status = 'timeout';
760
- executionInfo.stdout = stdout;
761
- executionInfo.stderr = stderr;
762
- executionInfo.output_truncated = true;
763
- executionInfo.completed_at = getCurrentTimestamp();
764
- executionInfo.execution_time_ms = Date.now() - startTime;
765
- // Save output to FileManager
766
- try {
767
- const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
768
- executionInfo.output_id = outputFileId;
769
- }
770
- catch (error) {
771
- // Record file-save failures as critical errors and include them in execution info
772
- console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
773
- executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
774
- }
775
- this.executions.set(executionId, executionInfo);
776
- // Invoke timeout callback for background process
777
- if (this.backgroundProcessCallbacks.onTimeout) {
778
- setImmediate(async () => {
779
- try {
780
- const callback = this.backgroundProcessCallbacks.onTimeout;
781
- if (callback) {
782
- const result = callback(executionId, executionInfo);
783
- if (result instanceof Promise) {
784
- await result;
851
+ const timeout = options.timeoutSeconds !== undefined
852
+ ? setTimeout(async () => {
853
+ childProcess.kill('SIGTERM');
854
+ setTimeout(() => {
855
+ if (!childProcess.killed) {
856
+ childProcess.kill('SIGKILL');
857
+ }
858
+ }, 5000);
859
+ const executionInfo = this.executions.get(executionId);
860
+ if (executionInfo) {
861
+ executionInfo.status = 'timeout';
862
+ executionInfo.stdout = stdout;
863
+ executionInfo.stderr = stderr;
864
+ executionInfo.output_truncated = true;
865
+ executionInfo.completed_at = getCurrentTimestamp();
866
+ executionInfo.execution_time_ms = Date.now() - startTime;
867
+ // Save output to FileManager
868
+ try {
869
+ const outputFileId = await this.saveOutputToFile(executionId, stdout, stderr);
870
+ executionInfo.output_id = outputFileId;
871
+ }
872
+ catch (error) {
873
+ // Record file-save failures as critical errors and include them in execution info
874
+ console.error(`[CRITICAL] Failed to save output file for execution ${executionId}:`, error);
875
+ executionInfo.message = `Output file save failed: ${error instanceof Error ? error.message : String(error)}`;
876
+ }
877
+ this.executions.set(executionId, executionInfo);
878
+ // Invoke timeout callback for background process
879
+ if (this.backgroundProcessCallbacks.onTimeout) {
880
+ setImmediate(async () => {
881
+ try {
882
+ const callback = this.backgroundProcessCallbacks.onTimeout;
883
+ if (callback) {
884
+ const result = callback(executionId, executionInfo);
885
+ if (result instanceof Promise) {
886
+ await result;
887
+ }
785
888
  }
786
889
  }
787
- }
788
- catch (callbackError) {
789
- // Record callback errors in internal logs only
790
- // console.error('Background process timeout callback error:', callbackError);
791
- }
792
- });
890
+ catch (callbackError) {
891
+ // Record callback errors in internal logs only
892
+ // console.error('Background process timeout callback error:', callbackError);
893
+ }
894
+ });
895
+ }
793
896
  }
794
- }
795
- }, options.timeoutSeconds * 1000);
897
+ }, options.timeoutSeconds * 1000)
898
+ : undefined;
796
899
  // Collect output
797
900
  childProcess.stdout?.on('data', (data) => {
798
901
  stdout += data.toString();
@@ -804,7 +907,9 @@ export class ProcessManager {
804
907
  }
805
908
  // Handle process close
806
909
  childProcess.on('close', async (code) => {
807
- clearTimeout(timeout);
910
+ if (timeout) {
911
+ clearTimeout(timeout);
912
+ }
808
913
  if (childProcess.pid) {
809
914
  this.processes.delete(childProcess.pid);
810
915
  }
@@ -846,7 +951,9 @@ export class ProcessManager {
846
951
  }
847
952
  });
848
953
  childProcess.on('error', (error) => {
849
- clearTimeout(timeout);
954
+ if (timeout) {
955
+ clearTimeout(timeout);
956
+ }
850
957
  if (childProcess.pid) {
851
958
  this.processes.delete(childProcess.pid);
852
959
  }
@@ -880,24 +987,28 @@ export class ProcessManager {
880
987
  // Handle processes transitioned to background in adaptive mode
881
988
  handleAdaptiveBackgroundTransition(executionId, childProcess, options) {
882
989
  // Set timeout (final timeout)
883
- const timeout = setTimeout(async () => {
884
- childProcess.kill('SIGTERM');
885
- setTimeout(() => {
886
- if (!childProcess.killed) {
887
- childProcess.kill('SIGKILL');
990
+ const timeout = options.timeoutSeconds !== undefined
991
+ ? setTimeout(async () => {
992
+ childProcess.kill('SIGTERM');
993
+ setTimeout(() => {
994
+ if (!childProcess.killed) {
995
+ childProcess.kill('SIGKILL');
996
+ }
997
+ }, 5000);
998
+ const executionInfo = this.executions.get(executionId);
999
+ if (executionInfo) {
1000
+ executionInfo.status = 'timeout';
1001
+ executionInfo.completed_at = getCurrentTimestamp();
1002
+ // Keep existing output (already captured in adaptive mode)
1003
+ this.executions.set(executionId, executionInfo);
888
1004
  }
889
- }, 5000);
890
- const executionInfo = this.executions.get(executionId);
891
- if (executionInfo) {
892
- executionInfo.status = 'timeout';
893
- executionInfo.completed_at = getCurrentTimestamp();
894
- // Keep existing output (already captured in adaptive mode)
895
- this.executions.set(executionId, executionInfo);
896
- }
897
- }, options.timeoutSeconds * 1000);
1005
+ }, options.timeoutSeconds * 1000)
1006
+ : undefined;
898
1007
  // Handle process close
899
1008
  childProcess.on('close', async (code) => {
900
- clearTimeout(timeout);
1009
+ if (timeout) {
1010
+ clearTimeout(timeout);
1011
+ }
901
1012
  if (childProcess.pid) {
902
1013
  this.processes.delete(childProcess.pid);
903
1014
  }
@@ -933,7 +1044,9 @@ export class ProcessManager {
933
1044
  }
934
1045
  });
935
1046
  childProcess.on('error', (error) => {
936
- clearTimeout(timeout);
1047
+ if (timeout) {
1048
+ clearTimeout(timeout);
1049
+ }
937
1050
  if (childProcess.pid) {
938
1051
  this.processes.delete(childProcess.pid);
939
1052
  }