agent-pool-mcp 1.7.2 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -183,12 +183,20 @@ See [parallel-work guide](examples/parallel-work.md) and built-in `orchestrator`
183
183
 
184
184
  ## MCP Ecosystem
185
185
 
186
- Best used together with [**project-graph-mcp**](https://www.npmjs.com/package/project-graph-mcp) — AST-based codebase analysis:
186
+ Best used as part of [**mcp-agent-portal**](https://github.com/rnd-pro/mcp-agent-portal) — a unified MCP aggregator that bundles agent-pool, project-graph, and other servers behind a single config entry:
187
187
 
188
- | Layer | agent-pool-mcp | project-graph-mcp |
189
- |-------|---------------|-------------------|
190
- | **Primary IDE agent** | Delegates tasks, consults peer | Navigates codebase, runs analysis |
191
- | **Gemini CLI workers** | Executes delegated tasks | Available as MCP tool inside workers |
188
+ ```json
189
+ {
190
+ "mcpServers": {
191
+ "agent-portal": {
192
+ "command": "npx",
193
+ "args": ["-y", "mcp-agent-portal"]
194
+ }
195
+ }
196
+ }
197
+ ```
198
+
199
+ Also works standalone alongside [**project-graph-mcp**](https://www.npmjs.com/package/project-graph-mcp) — AST-based codebase analysis:
192
200
 
193
201
  ```json
194
202
  {
@@ -214,6 +222,7 @@ Best used together with [**project-graph-mcp**](https://www.npmjs.com/package/pr
214
222
  - [examples/parallel-work.md](examples/parallel-work.md) — Delegation patterns and best practices
215
223
 
216
224
  ## Related Projects
225
+ - [mcp-agent-portal](https://github.com/rnd-pro/mcp-agent-portal) — Unified MCP aggregator + web dashboard + AI agent runtime
217
226
  - [project-graph-mcp](https://github.com/rnd-pro/project-graph-mcp) — AST-based codebase analysis for AI agents
218
227
  - [Symbiote.js](https://github.com/symbiotejs/symbiote.js) — Isomorphic Reactive Web Components framework
219
228
  - [JSDA-Kit](https://github.com/rnd-pro/jsda-kit) — SSG/SSR toolkit for modern web applications
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-pool-mcp",
3
- "version": "1.7.2",
3
+ "version": "1.7.3",
4
4
  "type": "module",
5
5
  "description": "MCP Server for multi-agent task delegation and orchestration via Gemini CLI",
6
6
  "main": "index.js",
@@ -10,7 +10,7 @@ import { spawn, execFile } from 'node:child_process';
10
10
  import { trackChild, killGroup, untrackChild } from './process-manager.js';
11
11
  import { getRunner, loadConfig } from './config.js';
12
12
  import { buildSshSpawn, parseRemotePid } from './ssh.js';
13
- import { setTaskPid, updateTaskResult, pushTaskEvent } from '../tools/results.js';
13
+ import { setTaskPid, updateTaskResult, pushTaskEvent, pushTaskStderr } from '../tools/results.js';
14
14
 
15
15
  const DEFAULT_TIMEOUT_SEC = 600;
16
16
  const DEFAULT_APPROVAL_MODE = 'yolo';
@@ -160,6 +160,7 @@ export function runGeminiStreaming({ prompt, cwd, model, approvalMode, timeout,
160
160
 
161
161
  child.stderr.on('data', (chunk) => {
162
162
  stderrData += chunk.toString();
163
+ if (taskId) pushTaskStderr(taskId, chunk.toString());
163
164
  });
164
165
 
165
166
  child.on('close', (code) => {
@@ -77,6 +77,8 @@ export function createTask(taskId, prompt, waitHint, approvalMode) {
77
77
  waitHint: waitHint ?? null,
78
78
  pid: null,
79
79
  liveEvents: [],
80
+ lastEventAt: null,
81
+ stderr: '',
80
82
  });
81
83
  }
82
84
 
@@ -90,6 +92,7 @@ export function pushTaskEvent(taskId, event) {
90
92
  const entry = taskStore.get(taskId);
91
93
  if (entry && entry.status === 'running') {
92
94
  entry.liveEvents.push(event);
95
+ entry.lastEventAt = Date.now();
93
96
  // Ring buffer: keep only the last MAX_LIVE_EVENTS
94
97
  if (entry.liveEvents.length > MAX_LIVE_EVENTS) {
95
98
  entry.liveEvents = entry.liveEvents.slice(-MAX_LIVE_EVENTS);
@@ -97,6 +100,23 @@ export function pushTaskEvent(taskId, event) {
97
100
  }
98
101
  }
99
102
 
103
+ /**
104
+ * Append stderr data to a task (for diagnostics).
105
+ *
106
+ * @param {string} taskId
107
+ * @param {string} chunk - stderr chunk
108
+ */
109
+ export function pushTaskStderr(taskId, chunk) {
110
+ const entry = taskStore.get(taskId);
111
+ if (entry && entry.status === 'running') {
112
+ entry.stderr += chunk;
113
+ // Keep only last 2KB of stderr
114
+ if (entry.stderr.length > 2048) {
115
+ entry.stderr = entry.stderr.slice(-2048);
116
+ }
117
+ }
118
+ }
119
+
100
120
  /**
101
121
  * Associate a PID with a task (called after spawn).
102
122
  *
@@ -316,6 +336,68 @@ export function formatTaskResult(taskId) {
316
336
  progress = '\n\n⏳ *Cold start — Gemini CLI initialization takes ~15-20s*';
317
337
  }
318
338
 
339
+ // Time since last event — key diagnostic
340
+ let activityInfo = '';
341
+ if (entry.lastEventAt) {
342
+ const silentSec = ((Date.now() - entry.lastEventAt) / 1000).toFixed(0);
343
+ if (parseInt(silentSec) > 60) {
344
+ activityInfo = `\n⏱️ Last event ${silentSec}s ago — model may be thinking or rate-limited.`;
345
+ }
346
+ }
347
+
348
+ // Stderr diagnostics — parse rate limits, extract actionable info
349
+ let stderrInfo = '';
350
+ if (entry.stderr) {
351
+ const raw = entry.stderr;
352
+
353
+ // Count rate limit occurrences
354
+ const rateLimitCount = (raw.match(/429|Too Many Requests|RESOURCE_EXHAUSTED/gi) || []).length;
355
+
356
+ // Forward-compatible: extract retry delay if present in stderr.
357
+ // Currently Google SDK does NOT include retryDelayMs in console.error output,
358
+ // but if Gemini CLI or SDK adds it in the future, this parser will pick it up.
359
+ // Supported formats:
360
+ // retryDelay: '42s' (Google API RetryInfo)
361
+ // retryDelayMs: 42000 (Gemini CLI error object)
362
+ // Retry-After: 30 (HTTP header)
363
+ let retrySeconds = null;
364
+ const msMatch = raw.match(/retryDelayMs[:"'\s]*(\d+)/i);
365
+ const secMatch = raw.match(/retryDelay[:"'\s]*'?(\d+)s'?/i);
366
+ const headerMatch = raw.match(/Retry-After[:"'\s]*(\d+)/i);
367
+ if (msMatch) retrySeconds = Math.ceil(parseInt(msMatch[1]) / 1000);
368
+ else if (secMatch) retrySeconds = parseInt(secMatch[1]);
369
+ else if (headerMatch) retrySeconds = parseInt(headerMatch[1]);
370
+
371
+ if (rateLimitCount > 0) {
372
+ const parts = [`⚡ Rate limited (429 × ${rateLimitCount})`];
373
+ if (retrySeconds) {
374
+ const resetAt = new Date(Date.now() + retrySeconds * 1000);
375
+ const resetTime = resetAt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
376
+ parts.push(`resets ~${resetTime} (${retrySeconds}s)`);
377
+ }
378
+ stderrInfo = `\n${parts.join(' — ')}`;
379
+ } else {
380
+ // Show other errors (non-rate-limit)
381
+ const lines = raw.trim().split('\n')
382
+ .filter((l) => !l.includes('IDEClient') && !l.includes('YOLO mode') && !l.includes('Loaded cached') && l.trim())
383
+ .slice(-3);
384
+ if (lines.length > 0) {
385
+ stderrInfo = `\n📋 stderr: ${lines.join(' | ').substring(0, 200)}`;
386
+ }
387
+ }
388
+ }
389
+
390
+ // PID alive check
391
+ let pidStatus = '';
392
+ if (entry.pid) {
393
+ try {
394
+ process.kill(entry.pid, 0); // signal 0 = check alive
395
+ pidStatus = ' (alive)';
396
+ } catch {
397
+ pidStatus = ' (dead ⚠️)';
398
+ }
399
+ }
400
+
319
401
  // System load awareness during polling
320
402
  const load = getSystemLoad();
321
403
  const loadInfo = load.warning ? `\n\n${load.warning}` : '';
@@ -325,7 +407,7 @@ export function formatTaskResult(taskId) {
325
407
  return {
326
408
  content: [{
327
409
  type: 'text',
328
- text: `⏳ Task is still running (${elapsed}s elapsed, ${entry.liveEvents.length} events).\n\n- **Prompt**: ${entry.prompt.substring(0, 100)}...\n- **Mode**: ${modeLabel}${progress}\n\n💡 **${hint}**${loadInfo}\n\nCheck again later with \`get_task_result\`.`,
410
+ text: `⏳ Task is still running (${elapsed}s elapsed, ${entry.liveEvents.length} events).\n\n- **Prompt**: ${entry.prompt.substring(0, 100)}...\n- **Mode**: ${modeLabel}\n- **PID**: ${entry.pid ?? 'unknown'}${pidStatus}${activityInfo}${stderrInfo}${progress}\n\n💡 **${hint}**${loadInfo}\n\nCheck again later with \`get_task_result\`.`,
329
411
  }],
330
412
  };
331
413
  }
@@ -371,6 +453,20 @@ export function formatTaskResult(taskId) {
371
453
  if (result.errors?.length > 0) {
372
454
  sections.push(`### Errors\n\n${result.errors.join('\n')}`);
373
455
  }
456
+ // Include stderr diagnostics (rate limits, etc.)
457
+ if (entry.stderr) {
458
+ const rateLimitCount = (entry.stderr.match(/429|Too Many Requests|RESOURCE_EXHAUSTED/gi) || []).length;
459
+ if (rateLimitCount > 0) {
460
+ sections.push(`### Cause: Rate Limit\n\n⚡ **${rateLimitCount} rate limit errors (429)** during execution. Task failed because API quota was exhausted.\n\n💡 Wait a few minutes for quota to reset, then retry.`);
461
+ } else {
462
+ const stderrLines = entry.stderr.trim().split('\n')
463
+ .filter((l) => !l.includes('IDEClient') && !l.includes('YOLO mode') && !l.includes('Loaded cached') && l.trim())
464
+ .slice(-5);
465
+ if (stderrLines.length > 0) {
466
+ sections.push(`### Stderr\n\n\`\`\`\n${stderrLines.join('\n')}\n\`\`\``);
467
+ }
468
+ }
469
+ }
374
470
  const errorSignal = (result.errors ?? []).join(' ');
375
471
  sections.push(`### Recovery\n\n${classifyError(errorSignal || `exit code ${result.exitCode}`)}`);
376
472
  }