@yemi33/minions 0.1.2372 → 0.1.2373

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/engine/timeout.js CHANGED
@@ -234,6 +234,31 @@ function checkDeferredStranded(activeProcesses, config) {
234
234
  }
235
235
  }
236
236
 
237
+ // P-6e2a4c15 — cancel-vs-kill for pooled dispatches. A pooled dispatch's
238
+ // `activeProcesses` record (`info`) carries `_pooled: true` (stamped by
239
+ // engine.js#spawnAgent, P-1d8f0b93) and its `.proc` is a PooledAgentProcess
240
+ // facade whose `.pid` is the SHARED, long-lived ACP worker's real OS pid —
241
+ // not a per-dispatch child process. Every OS-level kill helper in this file
242
+ // (`shared.killGracefully`, `shared.killImmediate`, `_escalatePlatformKill`)
243
+ // ultimately runs `taskkill /F /T /PID <pid>` or SIGKILLs that pid's whole
244
+ // process tree. Doing that to a pooled dispatch would kill the shared worker
245
+ // out from under every OTHER dispatch waiting to reuse it from the pool —
246
+ // exactly the collateral-damage risk `PooledAgentProcess#kill()` (P-1d8f0b93)
247
+ // was built to avoid: it cancels the in-flight ACP turn and releases the
248
+ // worker back to the pool without touching the OS process. Route every kill
249
+ // request here through this helper so no call site can accidentally bypass
250
+ // that primitive for a pooled record.
251
+ function killTrackedProcess(info, { graceMs } = {}) {
252
+ if (!info || !info.proc) return;
253
+ if (info._pooled && typeof info.proc.kill === 'function') {
254
+ try { info.proc.kill(); }
255
+ catch (err) { try { log('warn', `Pooled dispatch cancel failed for ${info.agentId || '?'}: ${err.message}`); } catch {} }
256
+ return;
257
+ }
258
+ if (graceMs == null) shared.killImmediate(info.proc);
259
+ else shared.killGracefully(info.proc, graceMs);
260
+ }
261
+
237
262
  // W-mq066js7000fff1f-c (Gap C): runs the kill-retry escalation ladder for an
238
263
  // already-steered process whose `_steeringAt` is set but which hasn't exited.
239
264
  // Caller is `checkSteering` per-tick loop; `info` is the `activeProcesses`
@@ -253,7 +278,7 @@ function _runSteeringKillLadder(id, info, config) {
253
278
  if (attempts < cap) {
254
279
  if (attempts === 0) {
255
280
  log('warn', `Steering: ${info.agentId} (${id}) didn't exit ${Math.round(wait / 1000)}s after kill — retrying gracefully`);
256
- try { shared.killGracefully(info.proc, 5000); }
281
+ try { killTrackedProcess(info, { graceMs: 5000 }); }
257
282
  catch (err) { try { log('warn', `Steering kill retry (graceful) failed for ${info.agentId}: ${err.message}`); } catch {} }
258
283
  } else {
259
284
  log('warn', `Steering: ${info.agentId} (${id}) survived attempt ${attempts} — escalating (attempt ${attempts + 1}/${cap})`);
@@ -287,6 +312,13 @@ function _runSteeringKillLadder(id, info, config) {
287
312
  }
288
313
 
289
314
  function _escalatePlatformKill(info) {
315
+ // Pooled dispatches never reach the OS-level escalation path — cancel the
316
+ // ACP turn and release the worker instead of taskkill/SIGKILLing the
317
+ // shared worker process. See killTrackedProcess's comment above.
318
+ if (info?._pooled && info.proc && typeof info.proc.kill === 'function') {
319
+ try { info.proc.kill(); } catch { /* best-effort */ }
320
+ return;
321
+ }
290
322
  const pid = info?.proc?.pid;
291
323
  if (!pid) return;
292
324
  if (process.platform === 'win32') {
@@ -418,7 +450,7 @@ function checkSteering(config) {
418
450
  fs.appendFileSync(liveLogPath, `\n[steering] Message received but no session to resume. Killing agent — your message will be delivered on retry.\n`);
419
451
  } catch { /* optional */ }
420
452
 
421
- shared.killImmediate(info.proc);
453
+ killTrackedProcess(info);
422
454
  info._steeringAt = Date.now();
423
455
  info._steeringNoSession = true;
424
456
  continue;
@@ -442,7 +474,7 @@ function checkSteering(config) {
442
474
  } catch { /* best-effort */ }
443
475
  }
444
476
 
445
- shared.killImmediate(info.proc);
477
+ killTrackedProcess(info);
446
478
  }
447
479
  }
448
480
 
@@ -619,7 +651,7 @@ function checkTimeouts(config) {
619
651
  if (elapsed > itemTimeout) {
620
652
  log('warn', `Agent ${info.agentId} (${id}) hit hard timeout after ${Math.round(elapsed / 1000)}s — killing`);
621
653
  dispatch().updateAgentStatus(id, AGENT_STATUS.TIMED_OUT, `Hard timeout after ${Math.round(elapsed / 1000)}s`);
622
- shared.killGracefully(info.proc, 5000);
654
+ killTrackedProcess(info, { graceMs: 5000 });
623
655
  }
624
656
  }
625
657
 
@@ -700,7 +732,7 @@ function checkTimeouts(config) {
700
732
  runPostCompletionHooks(item, item.agent, processExitCode, fullLogForHooks, config, { detectPhantom: true, expectedNonce, completionNonceRequired }).catch(e => log('warn', 'post-completion hooks: ' + e.message));
701
733
 
702
734
  if (hasProcess) {
703
- shared.killImmediate(activeProcesses.get(item.id)?.proc);
735
+ killTrackedProcess(activeProcesses.get(item.id));
704
736
  activeProcesses.delete(item.id);
705
737
  }
706
738
  }
@@ -815,7 +847,7 @@ function checkTimeouts(config) {
815
847
  // Clear the cached session so retry does not re-enter the same stuck --resume path.
816
848
  try { shared.safeUnlink(path.join(AGENTS_DIR, item.agent, 'session.json')); } catch {}
817
849
  activeProcesses.delete(item.id);
818
- shared.killGracefully(procInfo.proc, 5000);
850
+ killTrackedProcess(procInfo, { graceMs: 5000 });
819
851
  deadItems.push({ item, reason, failureClass: FAILURE_CLASS.TIMEOUT });
820
852
  }
821
853
  }
@@ -867,7 +899,18 @@ function checkTimeouts(config) {
867
899
 
868
900
  if (!processAlive && (canReapDeadProcess || silentMs > staleOrphanTimeout) && (Date.now() > engineRestartGraceUntil || canReapDeadProcess)) {
869
901
  // Last-resort PID check: lost tracked handle but OS process may still be alive.
870
- if (isOsPidAliveForDispatch(item.id)) {
902
+ // P-3f5b7d26 — SKIPPED for pooled dispatches (item.pooled, persisted by
903
+ // engine.js#spawnAgent). The PID recorded for a pooled dispatch is the
904
+ // shared ACP worker's real OS pid (engine/acp-transport.js#spawnAcp),
905
+ // whose command line is literally `copilot --acp ...` — it would always
906
+ // pass isProcessCommandLineMatchingAgent's `cmdline.includes('copilot')`
907
+ // check below even though that worker has no relationship to THIS
908
+ // dispatch (a fresh engine's agent-worker-pool.js always cold-starts
909
+ // empty; the pool never reconnects a restart-era worker to any prior
910
+ // dispatch). Without this guard a coincidentally-still-alive (or
911
+ // PID-recycled) worker process would be misread as "still running",
912
+ // parking the dispatch forever instead of routing it to retry.
913
+ if (!item.pooled && isOsPidAliveForDispatch(item.id)) {
871
914
  // Cross-check the PID's command line: a recycled OS PID can be alive
872
915
  // under an unrelated process (Windows reuses PIDs aggressively). Without
873
916
  // this cmdline gate, the dispatch is parked indefinitely as 'active'
@@ -1018,4 +1061,5 @@ module.exports = {
1018
1061
  _resetSteeringStoreCacheForTest, _setSteeringStoreForTest,
1019
1062
  // exported for testing — steering clamp helpers and deferred checkpoint
1020
1063
  _clampSteeringDeferredMaxMs, _clampSteeringMaxKillRetries, _appendLiveOutputLine, deferSteeringUntilCheckpoint,
1064
+ killTrackedProcess, _escalatePlatformKill, // exported for testing — P-6e2a4c15 cancel-vs-kill
1021
1065
  };
package/engine.js CHANGED
@@ -36,6 +36,12 @@ const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS,
36
36
  FAILURE_CLASS, resolvePollFlag } = shared;
37
37
  const { resolveRuntime } = require('./engine/runtimes');
38
38
  const { assertStaleHeadOk, preApproveWorkspaceMcps } = require('./engine/spawn-agent');
39
+ // P-1d8f0b93 — fleet ACP worker pool + its child_process-shaped facade.
40
+ // NOTE: engine/agent-worker-pool.js is a DIFFERENT module from the
41
+ // dashboard's single-tab CC pool (engine.js must never import that one —
42
+ // see the cc-tab-pool wiring regression guard test).
43
+ const agentWorkerPool = require('./engine/agent-worker-pool');
44
+ const { PooledAgentProcess } = require('./engine/pooled-agent-process');
39
45
  const adoGitAuth = require('./engine/ado-git-auth');
40
46
  const queries = require('./engine/queries');
41
47
  const dispatchEvents = require('./engine/dispatch-events');
@@ -4458,6 +4464,18 @@ async function spawnAgent(dispatchItem, config) {
4458
4464
  const resolvedModel = runtime.resolveModel(shared.resolveAgentModel(agentConfig, engineConfig));
4459
4465
  const resolvedMaxBudget = shared.resolveAgentMaxBudget(agentConfig, engineConfig);
4460
4466
  const resolvedBare = shared.resolveAgentBareMode(agentConfig, engineConfig);
4467
+ // P-1d8f0b93 — pool a copilot dispatch through the persistent ACP worker
4468
+ // pool (engine/agent-worker-pool.js) instead of cold-spawning a fresh CLI
4469
+ // process, when the capability + config gate allows it. Structurally
4470
+ // copilot-only (runtime.capabilities.acpWorkerPool) — see
4471
+ // shared.resolveAgentUseWorkerPool for the exact precedence chain.
4472
+ const useAgentPool = shared.resolveAgentUseWorkerPool(engineConfig, runtime);
4473
+ if (useAgentPool) {
4474
+ // Idempotent — cheap to call on every pooled dispatch so a live config
4475
+ // change (agentAcpPoolSize / maxConcurrent) takes effect without a
4476
+ // separate engine-restart-only wiring hook.
4477
+ agentWorkerPool.setPoolSize(shared.resolveAgentAcpPoolSize(engineConfig));
4478
+ }
4461
4479
 
4462
4480
  // W-mpg6isvy000xca4d — On retry after FAILURE_CLASS.MODEL_UNAVAILABLE, swap
4463
4481
  // to the runtime-appropriate fallback model. Two paths gated on
@@ -4582,7 +4600,14 @@ async function spawnAgent(dispatchItem, config) {
4582
4600
  childEnv.MINIONS_NO_AUTO_OPEN = '1';
4583
4601
  // MCP-free copilot config so agents never pop a per-spawn Microsoft auth
4584
4602
  // window — see _applyAgentCopilotHome / shared.resolveAgentCopilotHome.
4585
- _applyAgentCopilotHome(childEnv);
4603
+ // P-1d8f0b93 — SKIPPED for pooled dispatches: the pooled ACP worker is a
4604
+ // long-lived process shared across many dispatches, so it always uses the
4605
+ // operator's REAL COPILOT_HOME (this is the isolation-removal half of the
4606
+ // pooling feature — MCP-auth-popup risk becomes bounded by pool size
4607
+ // instead of eliminated per-dispatch; documented tradeoff in the plan).
4608
+ if (!useAgentPool) {
4609
+ _applyAgentCopilotHome(childEnv);
4610
+ }
4586
4611
  // Keep agent scratch off a (possibly full) system drive — anchors TEMP/TMP to
4587
4612
  // the worktree volume. See _applyAgentTempEnv / shared.resolveAgentTempBaseDir.
4588
4613
  _applyAgentTempEnv(childEnv, project);
@@ -4614,6 +4639,14 @@ async function spawnAgent(dispatchItem, config) {
4614
4639
  const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
4615
4640
 
4616
4641
  const spawnArgs = [spawnScript, promptPath, sysPromptPath, ...args];
4642
+ // P-1d8f0b93 — the pooled-dispatch branch further down (agentWorkerPool
4643
+ // acquireWorker) accepts an optional `hermeticDirs` (workspace-manifest
4644
+ // `allowed_repos` beyond the primary cwd — see engine/agent-worker-pool.js
4645
+ // header doc). Repository harness discovery (skills/commands/--add-dir) is
4646
+ // now delegated entirely to the native runtime CLI (#817), so there is no
4647
+ // longer an engine-computed add-dir set to reuse here; leave it empty and
4648
+ // let agentWorkerPool's `_applyHermeticDirs` no-op as designed.
4649
+ const addDirs = [];
4617
4650
 
4618
4651
  // Live output file — stamped BEFORE child process is spawned (#W-mo248lkjwgsu).
4619
4652
  // Writing the stub pre-spawn lets the orphan detector distinguish three failure modes
@@ -4687,12 +4720,43 @@ async function spawnAgent(dispatchItem, config) {
4687
4720
  // exactly — the agent's process already starts here, but cd-ing around or
4688
4721
  // sub-shells can drift, so the env var is the stable anchor.
4689
4722
  childEnv.MINIONS_AGENT_CWD = cwd;
4690
- proc = runFile(process.execPath, spawnArgs, {
4691
- cwd,
4692
- stdio: ['pipe', 'pipe', 'pipe'],
4693
- env: childEnv,
4694
- detached: true,
4695
- });
4723
+ if (!useAgentPool) {
4724
+ proc = runFile(process.execPath, spawnArgs, {
4725
+ cwd,
4726
+ stdio: ['pipe', 'pipe', 'pipe'],
4727
+ env: childEnv,
4728
+ detached: true,
4729
+ });
4730
+ } else {
4731
+ // P-1d8f0b93 — pooled copilot dispatch. No separate spawn-agent.js
4732
+ // child process exists, so build the SAME combined prompt text that
4733
+ // process would have received via runtime.buildPrompt(...) directly
4734
+ // here, and hand it to a PooledAgentProcess facade instead of
4735
+ // cold-spawning. Everything downstream (stdout/stderr handlers,
4736
+ // onAgentClose, live-output.log, PID-file writing below) treats `proc`
4737
+ // identically to a real child_process either way.
4738
+ const pooledPromptText = runtime.buildPrompt(fullTaskPrompt, systemPrompt, { sessionId: cachedSessionId });
4739
+ proc = new PooledAgentProcess({
4740
+ pool: agentWorkerPool,
4741
+ dispatchId: id,
4742
+ cwd,
4743
+ model: effectiveModel,
4744
+ effort: requestedEffort,
4745
+ // Mirrors the CC worker pool's convention (dashboard.js) — a single
4746
+ // flat top-level config array, not per-agent-resolved.
4747
+ mcpServers: (engineConfig && engineConfig.mcpServers) || [],
4748
+ hermeticDirs: addDirs,
4749
+ promptText: pooledPromptText,
4750
+ });
4751
+ // The child_process PID file is normally written asynchronously by
4752
+ // spawn-agent.js itself (engine/spawn-agent.js:606) once it starts.
4753
+ // There is no such child process here, so the engine writes the
4754
+ // equivalent file itself as soon as the pool hands back a real OS PID
4755
+ // (mirrors that write: async, best-effort, same `String(pid)` shape).
4756
+ proc.on('acquired', () => {
4757
+ try { fs.writeFile(pidFilePath, String(proc.pid || ''), () => {}); } catch { /* best-effort */ }
4758
+ });
4759
+ }
4696
4760
  _phaseT.spawnCallEnd = Date.now();
4697
4761
 
4698
4762
  // Seed realActivityMap and stamp PID immediately — BEFORE any handlers (#W-mo25loq8kjer).
@@ -4720,6 +4784,11 @@ async function spawnAgent(dispatchItem, config) {
4720
4784
  _runtimeResumeAwaitingFirstOutput: true,
4721
4785
  } : {}),
4722
4786
  _pendingSteeringFiles: pendingSteering.entries,
4787
+ // P-1d8f0b93 — marks a pooled dispatch so engine/timeout.js's
4788
+ // cancel-vs-kill wiring (P-6e2a4c15, killTrackedProcess /
4789
+ // _escalatePlatformKill) can route `.kill()` to cancel+release the
4790
+ // ACP worker instead of SIGKILL/taskkill on the shared worker's OS pid.
4791
+ ...(useAgentPool ? { _pooled: true } : {}),
4723
4792
  };
4724
4793
  activeProcesses.set(id, initialProcInfo);
4725
4794
  registeredInActiveProcesses = true;
@@ -6008,6 +6077,11 @@ async function spawnAgent(dispatchItem, config) {
6008
6077
  // route output parsing through the right adapter. Also surfaces the choice
6009
6078
  // in dispatch.json for debugging multi-runtime fleets.
6010
6079
  item.runtimeName = runtimeName;
6080
+ // P-3f5b7d26 — persist pooled on the DISK record (activeProcesses/_pooled
6081
+ // is wiped on restart); cli.js's reattach path needs this since a pooled
6082
+ // ACP worker isn't spawned detached and a fresh pool cold-starts empty —
6083
+ // a live worker PID must never be mistaken for a resumable dispatch.
6084
+ if (useAgentPool) item.pooled = true;
6011
6085
  // W-mq5rwwss000f30a7 — persist the worktree path so the live-worktree
6012
6086
  // guard (shared.isWorktreePathLive) can correlate destructive callers
6013
6087
  // (removeWorktree, cleanup orphan-dir sweep, pool-return, quarantine)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2372",
3
+ "version": "0.1.2373",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"