neoagent 2.5.2-beta.9 → 3.0.1-beta.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 (52) hide show
  1. package/README.md +5 -1
  2. package/docs/integrations.md +11 -7
  3. package/flutter_app/lib/main_controller.dart +34 -0
  4. package/flutter_app/lib/main_security.dart +19 -0
  5. package/lib/schema_migrations.js +224 -0
  6. package/package.json +2 -2
  7. package/server/admin/admin.css +29 -0
  8. package/server/admin/admin.js +81 -1
  9. package/server/admin/index.html +97 -9
  10. package/server/db/database.js +57 -1
  11. package/server/public/.last_build_id +1 -1
  12. package/server/public/flutter_bootstrap.js +1 -1
  13. package/server/public/main.dart.js +32788 -32752
  14. package/server/routes/security.js +19 -0
  15. package/server/routes/settings.js +1 -0
  16. package/server/routes/skills.js +9 -5
  17. package/server/services/ai/deliverables/artifact_helpers.js +92 -9
  18. package/server/services/ai/deliverables/selector.js +17 -0
  19. package/server/services/ai/hooks.js +3 -2
  20. package/server/services/ai/learning.js +8 -3
  21. package/server/services/ai/loop/agent_engine_core.js +67 -0
  22. package/server/services/ai/loop/blank_recovery.js +37 -0
  23. package/server/services/ai/loop/conversation_loop.js +391 -252
  24. package/server/services/ai/loop/error_recovery.js +38 -0
  25. package/server/services/ai/loop/messaging_delivery.js +52 -6
  26. package/server/services/ai/loop/progress_classification.js +178 -0
  27. package/server/services/ai/loop/progress_monitor.js +6 -5
  28. package/server/services/ai/loop/tool_dispatch.js +1 -0
  29. package/server/services/ai/loopPolicy.js +15 -6
  30. package/server/services/ai/messagingFallback.js +23 -1
  31. package/server/services/ai/preModelCompaction.js +31 -0
  32. package/server/services/ai/repetitionGuard.js +47 -2
  33. package/server/services/ai/settings.js +8 -0
  34. package/server/services/ai/systemPrompt.js +24 -0
  35. package/server/services/ai/taskAnalysis.js +10 -0
  36. package/server/services/ai/toolEvidence.js +39 -3
  37. package/server/services/ai/toolResult.js +29 -0
  38. package/server/services/ai/toolRunner.js +262 -55
  39. package/server/services/ai/toolSelector.js +20 -3
  40. package/server/services/ai/tools.js +201 -31
  41. package/server/services/cli/shell_worker_pool.js +87 -5
  42. package/server/services/integrations/github/common.js +2 -2
  43. package/server/services/integrations/github/repos.js +123 -55
  44. package/server/services/manager.js +16 -0
  45. package/server/services/memory/manager.js +39 -24
  46. package/server/services/runtime/docker-vm-manager.js +21 -1
  47. package/server/services/runtime/manager.js +6 -2
  48. package/server/services/security/approval_gate_service.js +108 -5
  49. package/server/services/security/tool_categories.js +113 -4
  50. package/server/services/security/tool_security_hook.js +7 -2
  51. package/server/services/skills/runtime.js +2 -0
  52. package/server/services/workspace/manager.js +56 -0
@@ -881,16 +881,57 @@ function getAvailableTools(app, options = {}) {
881
881
  },
882
882
  {
883
883
  name: 'read_file',
884
- description: 'Read a file from the filesystem. Supports reading specific line ranges for large files.',
884
+ description: 'Read one workspace file. Supports line ranges for large files. Use workspace file tools for code inspection whenever the files are in the shared workspace.',
885
885
  parameters: {
886
886
  type: 'object',
887
887
  properties: {
888
- path: { type: 'string', description: 'Absolute or relative file path' },
888
+ path: { type: 'string', description: 'Absolute or relative path inside the per-user workspace' },
889
+ file_path: { type: 'string', description: 'Alias for path; accepted for compatibility with model tool calls' },
889
890
  start_line: { type: 'number', description: 'Starting line number (1-indexed, inclusive)' },
890
891
  end_line: { type: 'number', description: 'Ending line number (1-indexed, inclusive)' },
892
+ line_start: { type: 'number', description: 'Alias for start_line; accepted for compatibility with model tool calls' },
893
+ line_count: { type: 'number', description: 'Number of lines to read starting at start_line/line_start' },
891
894
  encoding: { type: 'string', description: 'File encoding (default utf-8)' }
892
895
  },
893
- required: ['path']
896
+ required: []
897
+ }
898
+ },
899
+ {
900
+ name: 'read_files',
901
+ description: 'Read multiple workspace files or line ranges in one call. Prefer this over repeated single-file reads or shell snippets when inspecting related files.',
902
+ parameters: {
903
+ type: 'object',
904
+ properties: {
905
+ files: {
906
+ type: 'array',
907
+ description: 'Files to read. Each item may be a path string or an object with path/file_path plus optional start_line/end_line or line_start/line_count.',
908
+ items: {
909
+ oneOf: [
910
+ { type: 'string', description: 'Path inside the per-user workspace' },
911
+ {
912
+ type: 'object',
913
+ properties: {
914
+ path: { type: 'string', description: 'Absolute or relative path inside the per-user workspace' },
915
+ file_path: { type: 'string', description: 'Alias for path' },
916
+ start_line: { type: 'number', description: 'Starting line number (1-indexed, inclusive)' },
917
+ end_line: { type: 'number', description: 'Ending line number (1-indexed, inclusive)' },
918
+ line_start: { type: 'number', description: 'Alias for start_line' },
919
+ line_count: { type: 'number', description: 'Number of lines to read from start_line/line_start' },
920
+ encoding: { type: 'string', description: 'File encoding (default utf-8)' }
921
+ },
922
+ required: []
923
+ }
924
+ ]
925
+ }
926
+ },
927
+ paths: {
928
+ type: 'array',
929
+ items: { type: 'string' },
930
+ description: 'Convenience alias for reading whole files by path.'
931
+ },
932
+ encoding: { type: 'string', description: 'Default file encoding for items without encoding (default utf-8)' }
933
+ },
934
+ required: []
894
935
  }
895
936
  },
896
937
  {
@@ -900,10 +941,11 @@ function getAvailableTools(app, options = {}) {
900
941
  type: 'object',
901
942
  properties: {
902
943
  path: { type: 'string', description: 'File path' },
944
+ file_path: { type: 'string', description: 'Alias for path; accepted for compatibility with model tool calls' },
903
945
  content: { type: 'string', description: 'Content to write' },
904
946
  mode: { type: 'string', enum: ['write', 'append'], description: 'Write mode (default write)' }
905
947
  },
906
- required: ['path', 'content']
948
+ required: ['content']
907
949
  }
908
950
  },
909
951
  {
@@ -913,46 +955,68 @@ function getAvailableTools(app, options = {}) {
913
955
  type: 'object',
914
956
  properties: {
915
957
  path: { type: 'string', description: 'File path' },
958
+ file_path: { type: 'string', description: 'Alias for path; accepted for compatibility with model tool calls' },
916
959
  edits: {
917
960
  type: 'array',
918
961
  items: {
919
962
  type: 'object',
920
963
  properties: {
921
964
  oldText: { type: 'string', description: 'The exact text to replace.' },
922
- newText: { type: 'string', description: 'The replacement text.' }
965
+ newText: { type: 'string', description: 'The replacement text.' },
966
+ old_text: { type: 'string', description: 'Alias for oldText' },
967
+ new_text: { type: 'string', description: 'Alias for newText' }
923
968
  },
924
- required: ['oldText', 'newText']
969
+ required: []
925
970
  },
926
971
  description: 'List of text replacements to apply.'
927
972
  }
928
973
  },
929
- required: ['path', 'edits']
974
+ required: ['edits']
975
+ }
976
+ },
977
+ {
978
+ name: 'replace_file_range',
979
+ description: 'Replace a 1-indexed inclusive line range in a workspace file. Use this when you know line numbers and exact-text edit_file would be brittle. Read the target range first, then replace only the intended lines.',
980
+ parameters: {
981
+ type: 'object',
982
+ properties: {
983
+ path: { type: 'string', description: 'File path inside the per-user workspace' },
984
+ file_path: { type: 'string', description: 'Alias for path; accepted for compatibility with model tool calls' },
985
+ start_line: { type: 'number', description: 'Starting line number (1-indexed, inclusive)' },
986
+ end_line: { type: 'number', description: 'Ending line number (1-indexed, inclusive)' },
987
+ startLine: { type: 'number', description: 'Alias for start_line' },
988
+ endLine: { type: 'number', description: 'Alias for end_line' },
989
+ content: { type: 'string', description: 'Replacement content for the range. Empty string deletes the range.' }
990
+ },
991
+ required: ['content']
930
992
  }
931
993
  },
932
994
  {
933
995
  name: 'list_directory',
934
- description: 'List files and directories with metadata (size, modified time).',
996
+ description: 'List workspace files and directories with metadata. Omit path to list the workspace root.',
935
997
  parameters: {
936
998
  type: 'object',
937
999
  properties: {
938
- path: { type: 'string', description: 'Directory path' },
1000
+ path: { type: 'string', description: 'Directory path inside the workspace (default ".")' },
939
1001
  recursive: { type: 'boolean', description: 'List recursively' },
940
1002
  depth: { type: 'number', description: 'Maximum recursion depth (default 1, max 5)' }
941
1003
  },
942
- required: ['path']
1004
+ required: []
943
1005
  }
944
1006
  },
945
1007
  {
946
1008
  name: 'search_files',
947
- description: 'Search for text patterns across files in a directory (recursive).',
1009
+ description: 'Search for text across workspace files recursively. Omit path to search the workspace root.',
948
1010
  parameters: {
949
1011
  type: 'object',
950
1012
  properties: {
951
- path: { type: 'string', description: 'Directory to search in' },
1013
+ path: { type: 'string', description: 'Directory to search in (default ".")' },
952
1014
  query: { type: 'string', description: 'Text or regex pattern to search for' },
953
- include: { type: 'string', description: 'Glob pattern for files to include (e.g. "*.js")' }
1015
+ include: { type: 'string', description: 'Glob pattern for files to include (e.g. "*.js")' },
1016
+ maxDepth: { type: 'number', description: 'Maximum directory depth (default 5, max 10)' },
1017
+ maxFileSize: { type: 'number', description: 'Maximum file size in bytes to scan (default 1048576)' }
954
1018
  },
955
- required: ['path', 'query']
1019
+ required: ['query']
956
1020
  }
957
1021
  },
958
1022
  {
@@ -1141,7 +1205,7 @@ function getAvailableTools(app, options = {}) {
1141
1205
  },
1142
1206
  {
1143
1207
  name: 'create_task',
1144
- description: 'Create a background task with a named trigger and self-contained prompt.',
1208
+ description: 'Create a future, recurring, monitored, or background automation with a named trigger and self-contained prompt. Do not use for immediate short answers.',
1145
1209
  parameters: {
1146
1210
  type: 'object',
1147
1211
  properties: {
@@ -1160,12 +1224,12 @@ function getAvailableTools(app, options = {}) {
1160
1224
  },
1161
1225
  {
1162
1226
  name: 'list_tasks',
1163
- description: 'List all tasks for this user and agent.',
1227
+ description: 'List saved background/scheduled tasks for this user and agent. Do not use for ordinary immediate replies.',
1164
1228
  parameters: { type: 'object', properties: {} }
1165
1229
  },
1166
1230
  {
1167
1231
  name: 'delete_task',
1168
- description: 'Delete a task by its ID.',
1232
+ description: 'Delete a saved background/scheduled task by its ID.',
1169
1233
  parameters: {
1170
1234
  type: 'object',
1171
1235
  properties: {
@@ -1176,7 +1240,7 @@ function getAvailableTools(app, options = {}) {
1176
1240
  },
1177
1241
  {
1178
1242
  name: 'update_task',
1179
- description: 'Update an existing task, including its trigger, prompt, or enabled state.',
1243
+ description: 'Update an existing saved background/scheduled task, including its trigger, prompt, or enabled state.',
1180
1244
  parameters: {
1181
1245
  type: 'object',
1182
1246
  properties: {
@@ -1500,6 +1564,33 @@ function getAvailableTools(app, options = {}) {
1500
1564
  return compacted;
1501
1565
  }
1502
1566
 
1567
+ function firstStringArg(args = {}, names = []) {
1568
+ for (const name of names) {
1569
+ const value = args?.[name];
1570
+ if (typeof value === 'string' && value.trim()) return value;
1571
+ }
1572
+ return '';
1573
+ }
1574
+
1575
+ function normalizeReadFileArgs(args = {}) {
1576
+ const startLine = args.start_line ?? args.line_start ?? args.startLine ?? args.lineStart;
1577
+ let endLine = args.end_line ?? args.line_end ?? args.endLine ?? args.lineEnd;
1578
+ const lineCount = args.line_count ?? args.lineCount;
1579
+ if (endLine == null && startLine != null && lineCount != null) {
1580
+ const start = Number(startLine);
1581
+ const count = Number(lineCount);
1582
+ if (Number.isInteger(start) && Number.isInteger(count) && count > 0) {
1583
+ endLine = start + count - 1;
1584
+ }
1585
+ }
1586
+ return {
1587
+ path: firstStringArg(args, ['path', 'file_path', 'filePath', 'filename']),
1588
+ encoding: args.encoding || 'utf-8',
1589
+ start_line: startLine,
1590
+ end_line: endLine,
1591
+ };
1592
+ }
1593
+
1503
1594
  /**
1504
1595
  * Executes a tool by name.
1505
1596
  * @param {string} toolName - Name of the tool.
@@ -2297,12 +2388,58 @@ async function executeTool(toolName, args, context, engine) {
2297
2388
  try {
2298
2389
  const workspace = wc();
2299
2390
  if (!workspace) return { error: 'Workspace service is unavailable.' };
2300
- return workspace.readFile(userId, {
2301
- path: args.path,
2302
- encoding: args.encoding || 'utf-8',
2303
- start_line: args.start_line,
2304
- end_line: args.end_line,
2391
+ const normalizedArgs = normalizeReadFileArgs(args);
2392
+ if (!normalizedArgs.path) {
2393
+ return {
2394
+ error: 'read_file requires path or file_path. Keep source files in the workspace; for repository work clone or move the checkout into the workspace before using file tools.',
2395
+ };
2396
+ }
2397
+ return workspace.readFile(userId, normalizedArgs);
2398
+ } catch (err) {
2399
+ return { error: err.message };
2400
+ }
2401
+ }
2402
+
2403
+ case 'read_files': {
2404
+ try {
2405
+ const workspace = wc();
2406
+ if (!workspace) return { error: 'Workspace service is unavailable.' };
2407
+ const entries = Array.isArray(args.files)
2408
+ ? args.files
2409
+ : (Array.isArray(args.paths) ? args.paths : []);
2410
+ if (!entries.length) {
2411
+ return { error: 'read_files requires a non-empty files or paths array.' };
2412
+ }
2413
+ const maxFiles = 8;
2414
+ const results = entries.slice(0, maxFiles).map((entry, index) => {
2415
+ const fileArgs = typeof entry === 'string'
2416
+ ? { path: entry }
2417
+ : (entry && typeof entry === 'object' ? entry : {});
2418
+ const normalizedArgs = normalizeReadFileArgs({
2419
+ ...fileArgs,
2420
+ encoding: fileArgs.encoding || args.encoding || 'utf-8',
2421
+ });
2422
+ if (!normalizedArgs.path) {
2423
+ return {
2424
+ index,
2425
+ success: false,
2426
+ error: 'Each read_files item requires path or file_path.',
2427
+ };
2428
+ }
2429
+ const result = workspace.readFile(userId, normalizedArgs);
2430
+ return {
2431
+ index,
2432
+ requestedPath: normalizedArgs.path,
2433
+ success: !result?.error,
2434
+ ...result,
2435
+ };
2305
2436
  });
2437
+ return {
2438
+ success: results.every((result) => result.success !== false && !result.error),
2439
+ count: results.length,
2440
+ truncated: entries.length > maxFiles,
2441
+ results,
2442
+ };
2306
2443
  } catch (err) {
2307
2444
  return { error: err.message };
2308
2445
  }
@@ -2312,8 +2449,10 @@ async function executeTool(toolName, args, context, engine) {
2312
2449
  try {
2313
2450
  const workspace = wc();
2314
2451
  if (!workspace) return { error: 'Workspace service is unavailable.' };
2452
+ const targetPath = args.path || args.file_path;
2453
+ if (!targetPath) return { success: false, error: 'write_file requires path or file_path.' };
2315
2454
  return workspace.writeFile(userId, {
2316
- path: args.path,
2455
+ path: targetPath,
2317
2456
  content: args.content,
2318
2457
  mode: args.mode,
2319
2458
  });
@@ -2326,9 +2465,38 @@ async function executeTool(toolName, args, context, engine) {
2326
2465
  try {
2327
2466
  const workspace = wc();
2328
2467
  if (!workspace) return { error: 'Workspace service is unavailable.' };
2468
+ const targetPath = args.path || args.file_path;
2469
+ if (!targetPath) return { success: false, error: 'edit_file requires path or file_path.' };
2470
+ const edits = Array.isArray(args.edits)
2471
+ ? args.edits.map((edit) => ({
2472
+ ...edit,
2473
+ oldText: edit?.oldText ?? edit?.old_text,
2474
+ newText: edit?.newText ?? edit?.new_text,
2475
+ }))
2476
+ : [];
2329
2477
  return workspace.editFile(userId, {
2330
- path: args.path,
2331
- edits: args.edits,
2478
+ path: targetPath,
2479
+ edits,
2480
+ });
2481
+ } catch (err) {
2482
+ return { error: err.message };
2483
+ }
2484
+ }
2485
+
2486
+ case 'replace_file_range': {
2487
+ try {
2488
+ const workspace = wc();
2489
+ if (!workspace) return { error: 'Workspace service is unavailable.' };
2490
+ if (typeof workspace.replaceFileRange !== 'function') {
2491
+ return { error: 'Workspace service does not support replace_file_range.' };
2492
+ }
2493
+ const targetPath = args.path || args.file_path;
2494
+ if (!targetPath) return { success: false, error: 'replace_file_range requires path or file_path.' };
2495
+ return workspace.replaceFileRange(userId, {
2496
+ path: targetPath,
2497
+ start_line: args.start_line ?? args.startLine,
2498
+ end_line: args.end_line ?? args.endLine,
2499
+ content: args.content,
2332
2500
  });
2333
2501
  } catch (err) {
2334
2502
  return { error: err.message };
@@ -2357,6 +2525,8 @@ async function executeTool(toolName, args, context, engine) {
2357
2525
  path: args.path,
2358
2526
  query: args.query,
2359
2527
  include: args.include,
2528
+ maxDepth: args.maxDepth ?? args.max_depth,
2529
+ maxFileSize: args.maxFileSize ?? args.max_file_size,
2360
2530
  });
2361
2531
  } catch (err) {
2362
2532
  return { error: err.message };
@@ -2418,25 +2588,25 @@ async function executeTool(toolName, args, context, engine) {
2418
2588
  const { SkillRunner } = require('./toolRunner');
2419
2589
  const sharedRunner = sk();
2420
2590
  if (sharedRunner) {
2421
- const result = sharedRunner.createSkill(args.name, args.description, args.instructions, args.metadata);
2591
+ const result = sharedRunner.createSkill(userId, args.name, args.description, args.instructions, args.metadata);
2422
2592
  return result;
2423
2593
  }
2424
2594
  const runner = new SkillRunner();
2425
2595
  await runner.loadSkills();
2426
- return runner.createSkill(args.name, args.description, args.instructions, args.metadata);
2596
+ return runner.createSkill(userId, args.name, args.description, args.instructions, args.metadata);
2427
2597
  }
2428
2598
 
2429
2599
  case 'list_skills': {
2430
2600
  const skillRunner = sk();
2431
2601
  if (!skillRunner) return { error: 'Skill runner not available' };
2432
- const all = skillRunner.getAll();
2602
+ const all = skillRunner.getAll(userId);
2433
2603
  return { skills: all, count: all.length };
2434
2604
  }
2435
2605
 
2436
2606
  case 'update_skill': {
2437
2607
  const skillRunner = sk();
2438
2608
  if (!skillRunner) return { error: 'Skill runner not available' };
2439
- return skillRunner.updateSkill(args.name, {
2609
+ return skillRunner.updateSkill(userId, args.name, {
2440
2610
  description: args.description,
2441
2611
  instructions: args.instructions,
2442
2612
  metadata: args.metadata
@@ -2446,7 +2616,7 @@ async function executeTool(toolName, args, context, engine) {
2446
2616
  case 'delete_skill': {
2447
2617
  const skillRunner = sk();
2448
2618
  if (!skillRunner) return { error: 'Skill runner not available' };
2449
- return skillRunner.deleteSkill(args.name);
2619
+ return skillRunner.deleteSkill(userId, args.name);
2450
2620
  }
2451
2621
 
2452
2622
  case 'think': {
@@ -5,6 +5,60 @@ const { randomUUID } = require('crypto');
5
5
  const path = require('path');
6
6
 
7
7
  const WORKER_SCRIPT = path.resolve(__dirname, 'shell_worker.js');
8
+ const DEFAULT_WORKER_ENV_KEYS = Object.freeze([
9
+ 'HOME',
10
+ 'PATH',
11
+ 'SHELL',
12
+ 'TMPDIR',
13
+ 'TMP',
14
+ 'TEMP',
15
+ 'TERM',
16
+ 'LANG',
17
+ 'LC_ALL',
18
+ 'LC_CTYPE',
19
+ 'USER',
20
+ 'LOGNAME',
21
+ 'COLORTERM',
22
+ 'SystemRoot',
23
+ 'ComSpec',
24
+ 'PATHEXT',
25
+ 'WINDIR',
26
+ ]);
27
+
28
+ function normalizeExtraEnvKeys(extraEnvKeys = process.env.NEOAGENT_SHELL_WORKER_ENV_ALLOWLIST || '') {
29
+ if (Array.isArray(extraEnvKeys)) {
30
+ return extraEnvKeys.map((key) => String(key || '').trim()).filter(Boolean);
31
+ }
32
+ return String(extraEnvKeys || '')
33
+ .split(',')
34
+ .map((key) => key.trim())
35
+ .filter(Boolean);
36
+ }
37
+
38
+ function buildWorkerEnv(baseEnv = process.env, extraEnvKeys) {
39
+ const allowedKeys = new Set([
40
+ ...DEFAULT_WORKER_ENV_KEYS,
41
+ ...normalizeExtraEnvKeys(extraEnvKeys),
42
+ ]);
43
+ const workerEnv = {};
44
+ for (const key of allowedKeys) {
45
+ if (Object.prototype.hasOwnProperty.call(baseEnv, key) && baseEnv[key] != null) {
46
+ workerEnv[key] = String(baseEnv[key]);
47
+ }
48
+ }
49
+ return workerEnv;
50
+ }
51
+
52
+ function createShutdownResult(message, killed = false) {
53
+ return {
54
+ exitCode: -1,
55
+ stdout: '',
56
+ stderr: message,
57
+ killed,
58
+ timedOut: false,
59
+ durationMs: 0,
60
+ };
61
+ }
8
62
 
9
63
  class ShellWorkerPool {
10
64
  /**
@@ -21,6 +75,7 @@ class ShellWorkerPool {
21
75
  this._queue = [];
22
76
  /** @type {Map<string, Function>} requestId → resolve */
23
77
  this._pending = new Map();
78
+ this._shuttingDown = false;
24
79
 
25
80
  for (let i = 0; i < this._size; i++) {
26
81
  this._spawnWorker();
@@ -31,7 +86,7 @@ class ShellWorkerPool {
31
86
  const proc = fork(this._workerScript, [], {
32
87
  detached: false,
33
88
  stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
34
- env: { ...process.env },
89
+ env: buildWorkerEnv(process.env),
35
90
  });
36
91
 
37
92
  const workerEntry = { proc, busy: false, pendingRequestId: null };
@@ -52,7 +107,6 @@ class ShellWorkerPool {
52
107
  });
53
108
 
54
109
  proc.on('exit', (code) => {
55
- console.warn(`[ShellWorkerPool] Worker exited (code=${code}), respawning`);
56
110
  const idx = this._workers.indexOf(workerEntry);
57
111
  if (idx !== -1) this._workers.splice(idx, 1);
58
112
 
@@ -61,10 +115,14 @@ class ShellWorkerPool {
61
115
  const resolve = this._pending.get(workerEntry.pendingRequestId);
62
116
  if (resolve) {
63
117
  this._pending.delete(workerEntry.pendingRequestId);
64
- resolve({ exitCode: -1, stdout: '', stderr: 'Worker process crashed', killed: true, timedOut: false });
118
+ const message = this._shuttingDown ? 'Server shutting down' : 'Worker process crashed';
119
+ resolve(createShutdownResult(message, true));
65
120
  }
66
121
  }
67
122
 
123
+ if (this._shuttingDown) return;
124
+
125
+ console.warn(`[ShellWorkerPool] Worker exited (code=${code}), respawning`);
68
126
  this._spawnWorker();
69
127
  });
70
128
 
@@ -113,13 +171,37 @@ class ShellWorkerPool {
113
171
 
114
172
  /** Gracefully terminate all workers. */
115
173
  shutdown() {
174
+ if (this._shuttingDown) return;
175
+ this._shuttingDown = true;
176
+
177
+ const shutdownResult = createShutdownResult('Server shutting down', true);
178
+ for (const job of this._queue) {
179
+ job.resolve(shutdownResult);
180
+ }
181
+ this._queue = [];
182
+
183
+ for (const w of this._workers) {
184
+ if (w.pendingRequestId) {
185
+ const resolve = this._pending.get(w.pendingRequestId);
186
+ if (resolve) {
187
+ this._pending.delete(w.pendingRequestId);
188
+ resolve(shutdownResult);
189
+ }
190
+ w.pendingRequestId = null;
191
+ w.busy = false;
192
+ }
193
+ }
194
+
116
195
  for (const w of this._workers) {
117
196
  try { w.proc.kill(); } catch {}
118
197
  }
119
198
  this._workers = [];
120
- this._queue = [];
121
199
  this._pending.clear();
122
200
  }
123
201
  }
124
202
 
125
- module.exports = { ShellWorkerPool };
203
+ module.exports = {
204
+ ShellWorkerPool,
205
+ DEFAULT_WORKER_ENV_KEYS,
206
+ buildWorkerEnv,
207
+ };
@@ -42,7 +42,7 @@ async function githubApiRequest(auth, options = {}) {
42
42
  'X-GitHub-Api-Version': '2022-11-28',
43
43
  };
44
44
 
45
- if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
45
+ if (body) {
46
46
  headers['Content-Type'] = 'application/json';
47
47
  }
48
48
 
@@ -103,4 +103,4 @@ module.exports = {
103
103
  buildPaginationParams,
104
104
  githubApiRequest,
105
105
  parseOwnerRepo,
106
- };
106
+ };