@yemi33/minions 0.1.2382 → 0.1.2384
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/bin/minions.js +1 -0
- package/dashboard/js/refresh.js +2 -1
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +37 -19
- package/docs/completion-reports.md +20 -1
- package/docs/keep-processes.md +7 -2
- package/docs/live-checkout-mode.md +7 -7
- package/docs/managed-spawn.md +14 -1
- package/docs/runtime-adapters.md +61 -26
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +36 -0
- package/engine/acp-transport.js +273 -58
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +278 -54
- package/engine/cc-worker-pool.js +12 -3
- package/engine/cleanup.js +15 -11
- package/engine/cli.js +56 -28
- package/engine/comment-format.js +51 -14
- package/engine/create-pr-worktree.js +57 -79
- package/engine/dispatch.js +7 -2
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/keep-process-sweep.js +131 -9
- package/engine/lifecycle.js +126 -45
- package/engine/live-checkout.js +4 -2
- package/engine/llm.js +59 -83
- package/engine/managed-spawn.js +112 -5
- package/engine/memory-store.js +3 -1
- package/engine/playbook.js +4 -6
- package/engine/pooled-agent-process.js +512 -45
- package/engine/pr-action.js +6 -8
- package/engine/process-utils.js +154 -18
- package/engine/runtimes/claude.js +94 -25
- package/engine/runtimes/codex.js +1 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +97 -9
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +349 -82
- package/engine/spawn-agent.js +85 -116
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine/timeout.js +14 -10
- package/engine/worktree-gc.js +55 -46
- package/engine.js +418 -241
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +2 -2
- package/prompts/cc-system.md +11 -11
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* worker exists and the pool is already at its configured size.
|
|
19
19
|
* WorkerHandle: {
|
|
20
20
|
* dispatchId,
|
|
21
|
+
* currentModel, // ACP-selected session model
|
|
21
22
|
* stream(promptText, opts) → Promise, // ACP session/prompt turn
|
|
22
23
|
* cancel(), // ACP session/cancel
|
|
23
24
|
* release(), // return the worker to the
|
|
@@ -29,14 +30,15 @@
|
|
|
29
30
|
*
|
|
30
31
|
* Assignment strategy (acquireWorker), per the P-7a1c2e4f spike's findings:
|
|
31
32
|
*
|
|
32
|
-
* 1. A FREE worker whose
|
|
33
|
+
* 1. A FREE worker whose MCP config, process environment, and process cwd
|
|
34
|
+
* match the request wins
|
|
33
35
|
* first — reused via a fresh `session/new` (worker.newSession), no
|
|
34
36
|
* respawn, no wasted initialize handshake.
|
|
35
37
|
* 2. Otherwise any OTHER free worker is evicted (closed) and a replacement
|
|
36
38
|
* is spawned fresh with the requested mcpServers/cwd — MCP servers are
|
|
37
39
|
* resolved at `copilot --acp` process boot (via $COPILOT_HOME), so a
|
|
38
|
-
* changed local/stdio server set requires a full respawn,
|
|
39
|
-
* new session (spike (c)).
|
|
40
|
+
* changed local/stdio server set or process cwd requires a full respawn,
|
|
41
|
+
* not just a new session (spike (c)).
|
|
40
42
|
* 3. Otherwise, if the pool has room (fewer than `poolSize` workers
|
|
41
43
|
* allocated across free+busy+in-flight-spawn), spawn a brand new
|
|
42
44
|
* worker.
|
|
@@ -58,7 +60,9 @@
|
|
|
58
60
|
* No new npm deps — Node built-ins only.
|
|
59
61
|
*/
|
|
60
62
|
|
|
63
|
+
const path = require('path');
|
|
61
64
|
const transport = require('./acp-transport');
|
|
65
|
+
const { buildWorkerArgs } = require('./runtimes');
|
|
62
66
|
const { Worker, hashMcpServers, hashProcessContext } = transport;
|
|
63
67
|
|
|
64
68
|
// Test seam — mirrors engine/cc-worker-pool.js's `_internals` pattern, but is
|
|
@@ -67,6 +71,11 @@ const { Worker, hashMcpServers, hashProcessContext } = transport;
|
|
|
67
71
|
const _internals = {
|
|
68
72
|
spawnAcp: transport.spawnAcp,
|
|
69
73
|
killImmediate: transport.killImmediate,
|
|
74
|
+
killRoot: transport.killRoot,
|
|
75
|
+
async getProcessStartedAt(pid) {
|
|
76
|
+
const processes = await require('./shared').listAllProcessesAsync();
|
|
77
|
+
return Number(processes.find((entry) => entry.pid === pid)?.startedAt) || 0;
|
|
78
|
+
},
|
|
70
79
|
now() { return Date.now(); },
|
|
71
80
|
};
|
|
72
81
|
|
|
@@ -81,8 +90,10 @@ let _poolSize = DEFAULT_POOL_SIZE;
|
|
|
81
90
|
|
|
82
91
|
function setPoolSize(n) {
|
|
83
92
|
const parsed = Number(n);
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
const nextSize = Math.floor(parsed);
|
|
94
|
+
if (!Number.isFinite(parsed) || nextSize <= 0) return _poolSize;
|
|
95
|
+
_poolSize = nextSize;
|
|
96
|
+
_trimFreeWorkers();
|
|
86
97
|
_pump();
|
|
87
98
|
return _poolSize;
|
|
88
99
|
}
|
|
@@ -107,17 +118,57 @@ const _free = []; // Worker[] — idle, unleased
|
|
|
107
118
|
const _busy = new Map(); // dispatchId → Worker — currently leased
|
|
108
119
|
const _queue = []; // pending acquire requests, FIFO
|
|
109
120
|
const _leaseContexts = new Map(); // dispatchId → mutable session-only context
|
|
121
|
+
const _acquiring = new Map(); // dispatchId → queued/reserved/setup request
|
|
122
|
+
const _starting = new Set(); // Worker instances still in ACP handshake
|
|
110
123
|
// Slots reserved for a worker that's mid-reuse/respawn/fresh-spawn — pulled
|
|
111
124
|
// out of `_free` (or not yet spawned) but not yet placed in `_busy`. Counted
|
|
112
125
|
// toward `_poolSize` so a burst of concurrent acquireWorker calls can't
|
|
113
126
|
// overshoot the configured pool size while spawns are still in flight.
|
|
114
127
|
let _reserved = 0;
|
|
115
128
|
let _draining = false;
|
|
129
|
+
let _enabled = true;
|
|
116
130
|
|
|
117
131
|
function _occupied() {
|
|
118
132
|
return _free.length + _busy.size + _reserved;
|
|
119
133
|
}
|
|
120
134
|
|
|
135
|
+
function _pathKey(value) {
|
|
136
|
+
const resolved = path.resolve(value || process.cwd());
|
|
137
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _closeWorker(worker) {
|
|
141
|
+
try {
|
|
142
|
+
if (worker.processStartedAt > 0) worker.close();
|
|
143
|
+
else worker.abandon();
|
|
144
|
+
} catch { /* already torn down */ }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _trimFreeWorkers() {
|
|
148
|
+
while (_free.length > 0 && _occupied() > _poolSize) {
|
|
149
|
+
_closeWorker(_free.pop());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _settleRequest(req, method, value) {
|
|
154
|
+
if (req.settled) return false;
|
|
155
|
+
req.settled = true;
|
|
156
|
+
try { req[method](value); } catch { /* already settled */ }
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _releaseReservation(req) {
|
|
161
|
+
if (!req.reserved) return;
|
|
162
|
+
req.reserved = false;
|
|
163
|
+
_reserved = Math.max(0, _reserved - 1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _acquisitionUnavailableError(req) {
|
|
167
|
+
if (req?.cancelled) return new Error('agent-worker-pool: acquisition cancelled');
|
|
168
|
+
if (_draining) return new Error('agent-worker-pool: draining');
|
|
169
|
+
return new Error('agent-worker-pool: disabled');
|
|
170
|
+
}
|
|
171
|
+
|
|
121
172
|
// ── Queue pump ──────────────────────────────────────────────────────────
|
|
122
173
|
//
|
|
123
174
|
// Fully synchronous per pass: each iteration either satisfies the head of
|
|
@@ -129,17 +180,48 @@ function _occupied() {
|
|
|
129
180
|
// no-op re-pump; failure frees the reservation and DOES let another queued
|
|
130
181
|
// request through).
|
|
131
182
|
function _pump() {
|
|
183
|
+
if (_draining || !_enabled) return;
|
|
132
184
|
while (_queue.length > 0) {
|
|
133
185
|
const req = _queue[0];
|
|
134
186
|
|
|
135
|
-
const
|
|
136
|
-
w.
|
|
187
|
+
const compatible = (w) => (
|
|
188
|
+
w.runtime === req.runtime
|
|
189
|
+
&& w.mcpServersHash === req.mcpServersHash
|
|
137
190
|
&& w.processContextHash === req.processContextHash
|
|
191
|
+
&& w.processCwdKey === req.processCwdKey
|
|
138
192
|
);
|
|
193
|
+
if (req.resumeSessionId) {
|
|
194
|
+
const busyOwnsSession = [..._busy.values()].some((worker) =>
|
|
195
|
+
worker.runtime === req.runtime && worker.sessionId === req.resumeSessionId);
|
|
196
|
+
const acquiringOwnsSession = [..._acquiring.values()].some((other) =>
|
|
197
|
+
other !== req
|
|
198
|
+
&& other.reserved
|
|
199
|
+
&& other.runtime === req.runtime
|
|
200
|
+
&& (other.resumeSessionId === req.resumeSessionId || other.worker?.sessionId === req.resumeSessionId));
|
|
201
|
+
if (busyOwnsSession || acquiringOwnsSession) break;
|
|
202
|
+
|
|
203
|
+
const sessionOwnerIdx = _free.findIndex((worker) =>
|
|
204
|
+
worker.runtime === req.runtime && worker.sessionId === req.resumeSessionId);
|
|
205
|
+
if (sessionOwnerIdx >= 0) {
|
|
206
|
+
_queue.shift();
|
|
207
|
+
const sessionOwner = _free.splice(sessionOwnerIdx, 1)[0];
|
|
208
|
+
_reserved++;
|
|
209
|
+
req.reserved = true;
|
|
210
|
+
if (compatible(sessionOwner) && _pathKey(sessionOwner.cwd) === _pathKey(req.cwd)) {
|
|
211
|
+
_reuseWorker(sessionOwner, req);
|
|
212
|
+
} else {
|
|
213
|
+
_respawnWorker(sessionOwner, req);
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const matchIdx = _free.findIndex(compatible);
|
|
139
220
|
if (matchIdx >= 0) {
|
|
140
221
|
_queue.shift();
|
|
141
222
|
const worker = _free.splice(matchIdx, 1)[0];
|
|
142
223
|
_reserved++;
|
|
224
|
+
req.reserved = true;
|
|
143
225
|
_reuseWorker(worker, req);
|
|
144
226
|
continue;
|
|
145
227
|
}
|
|
@@ -148,6 +230,7 @@ function _pump() {
|
|
|
148
230
|
_queue.shift();
|
|
149
231
|
const stale = _free.shift();
|
|
150
232
|
_reserved++;
|
|
233
|
+
req.reserved = true;
|
|
151
234
|
_respawnWorker(stale, req);
|
|
152
235
|
continue;
|
|
153
236
|
}
|
|
@@ -155,6 +238,7 @@ function _pump() {
|
|
|
155
238
|
if (_occupied() < _poolSize) {
|
|
156
239
|
_queue.shift();
|
|
157
240
|
_reserved++;
|
|
241
|
+
req.reserved = true;
|
|
158
242
|
_spawnFreshWorker(req);
|
|
159
243
|
continue;
|
|
160
244
|
}
|
|
@@ -166,40 +250,55 @@ function _pump() {
|
|
|
166
250
|
}
|
|
167
251
|
|
|
168
252
|
async function _reuseWorker(worker, req) {
|
|
253
|
+
req.worker = worker;
|
|
169
254
|
try {
|
|
170
|
-
|
|
255
|
+
if (req.resumeSessionId) {
|
|
256
|
+
await worker.loadSession(req.resumeSessionId, {
|
|
257
|
+
mcpServers: req.mcpServers,
|
|
258
|
+
model: req.model,
|
|
259
|
+
effort: req.effort,
|
|
260
|
+
cwd: req.cwd,
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
await worker.newSession({
|
|
264
|
+
mcpServers: req.mcpServers,
|
|
265
|
+
model: req.model,
|
|
266
|
+
effort: req.effort,
|
|
267
|
+
cwd: req.cwd,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
171
270
|
_trace(`dispatch=${req.dispatchId} reuse: worker=${worker.id} sessionId=${worker.sessionId}`);
|
|
172
|
-
|
|
173
|
-
if (_draining) {
|
|
174
|
-
|
|
175
|
-
req
|
|
271
|
+
_releaseReservation(req);
|
|
272
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
273
|
+
_closeWorker(worker);
|
|
274
|
+
_settleRequest(req, 'reject', _acquisitionUnavailableError(req));
|
|
176
275
|
return;
|
|
177
276
|
}
|
|
178
|
-
req
|
|
277
|
+
_settleRequest(req, 'resolve', worker);
|
|
179
278
|
} catch (err) {
|
|
180
|
-
|
|
279
|
+
_releaseReservation(req);
|
|
181
280
|
// The worker died mid-newSession (or the daemon rejected the call) —
|
|
182
281
|
// it's already unusable; don't return it to `_free`.
|
|
183
|
-
|
|
184
|
-
req
|
|
282
|
+
_closeWorker(worker);
|
|
283
|
+
_settleRequest(req, 'reject', err);
|
|
185
284
|
_pump();
|
|
186
285
|
}
|
|
187
286
|
}
|
|
188
287
|
|
|
189
288
|
async function _respawnWorker(stale, req) {
|
|
190
|
-
|
|
289
|
+
_closeWorker(stale);
|
|
191
290
|
try {
|
|
192
291
|
const worker = await _bootWorker(req);
|
|
193
|
-
|
|
194
|
-
if (_draining) {
|
|
195
|
-
|
|
196
|
-
req
|
|
292
|
+
_releaseReservation(req);
|
|
293
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
294
|
+
_closeWorker(worker);
|
|
295
|
+
_settleRequest(req, 'reject', _acquisitionUnavailableError(req));
|
|
197
296
|
return;
|
|
198
297
|
}
|
|
199
|
-
req
|
|
298
|
+
_settleRequest(req, 'resolve', worker);
|
|
200
299
|
} catch (err) {
|
|
201
|
-
|
|
202
|
-
req
|
|
300
|
+
_releaseReservation(req);
|
|
301
|
+
_settleRequest(req, 'reject', err);
|
|
203
302
|
_pump();
|
|
204
303
|
}
|
|
205
304
|
}
|
|
@@ -207,16 +306,16 @@ async function _respawnWorker(stale, req) {
|
|
|
207
306
|
async function _spawnFreshWorker(req) {
|
|
208
307
|
try {
|
|
209
308
|
const worker = await _bootWorker(req);
|
|
210
|
-
|
|
211
|
-
if (_draining) {
|
|
212
|
-
|
|
213
|
-
req
|
|
309
|
+
_releaseReservation(req);
|
|
310
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
311
|
+
_closeWorker(worker);
|
|
312
|
+
_settleRequest(req, 'reject', _acquisitionUnavailableError(req));
|
|
214
313
|
return;
|
|
215
314
|
}
|
|
216
|
-
req
|
|
315
|
+
_settleRequest(req, 'resolve', worker);
|
|
217
316
|
} catch (err) {
|
|
218
|
-
|
|
219
|
-
req
|
|
317
|
+
_releaseReservation(req);
|
|
318
|
+
_settleRequest(req, 'reject', err);
|
|
220
319
|
_pump();
|
|
221
320
|
}
|
|
222
321
|
}
|
|
@@ -229,7 +328,11 @@ async function _spawnFreshWorker(req) {
|
|
|
229
328
|
// whichever pool collection it's currently sitting in, then re-pumps so a
|
|
230
329
|
// queued request can spawn its replacement.
|
|
231
330
|
async function _bootWorker(req) {
|
|
331
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
332
|
+
throw _acquisitionUnavailableError(req);
|
|
333
|
+
}
|
|
232
334
|
const worker = new Worker({
|
|
335
|
+
runtime: req.runtime,
|
|
233
336
|
id: `agent-pool-${_nextWorkerId++}`,
|
|
234
337
|
model: req.model,
|
|
235
338
|
effort: req.effort,
|
|
@@ -237,19 +340,33 @@ async function _bootWorker(req) {
|
|
|
237
340
|
mcpServersHash: req.mcpServersHash,
|
|
238
341
|
processEnv: req.processEnv,
|
|
239
342
|
processContextHash: req.processContextHash,
|
|
343
|
+
workerOptions: req.workerOptions,
|
|
344
|
+
resumeSessionId: req.resumeSessionId,
|
|
240
345
|
cwd: req.cwd,
|
|
346
|
+
processCwd: req.processCwd,
|
|
241
347
|
internals: _internals,
|
|
242
348
|
trace: _trace,
|
|
243
349
|
});
|
|
350
|
+
worker.processCwdKey = req.processCwdKey;
|
|
351
|
+
req.worker = worker;
|
|
352
|
+
_starting.add(worker);
|
|
244
353
|
const initPromise = worker._spawnAndInit();
|
|
354
|
+
const identityPromise = worker.proc?.pid
|
|
355
|
+
? _internals.getProcessStartedAt(worker.proc.pid)
|
|
356
|
+
: Promise.resolve(0);
|
|
245
357
|
worker.initPromise = initPromise;
|
|
246
358
|
try {
|
|
247
|
-
await initPromise;
|
|
359
|
+
const [, processStartedAt] = await Promise.all([initPromise, identityPromise]);
|
|
360
|
+
if (worker.killed || processStartedAt <= 0) {
|
|
361
|
+
throw new Error('agent-worker-pool: worker process identity unavailable after startup');
|
|
362
|
+
}
|
|
363
|
+
worker.processStartedAt = processStartedAt;
|
|
248
364
|
} catch (err) {
|
|
249
|
-
|
|
365
|
+
_closeWorker(worker);
|
|
250
366
|
throw err;
|
|
251
367
|
} finally {
|
|
252
368
|
worker.initPromise = null;
|
|
369
|
+
_starting.delete(worker);
|
|
253
370
|
}
|
|
254
371
|
if (worker.proc) {
|
|
255
372
|
worker.proc.once('exit', () => _onWorkerExit(worker));
|
|
@@ -312,11 +429,13 @@ function _clearLeaseContext(dispatchId) {
|
|
|
312
429
|
}
|
|
313
430
|
|
|
314
431
|
async function acquireWorker({
|
|
315
|
-
dispatchId, cwd, model, effort, mcpServers, hermeticDirs,
|
|
432
|
+
runtime, dispatchId, cwd, processCwd, model, effort, mcpServers, hermeticDirs,
|
|
433
|
+
processEnv, workerOptions, sessionId, sessionContext,
|
|
316
434
|
} = {}) {
|
|
317
435
|
if (!dispatchId) throw new Error('agent-worker-pool.acquireWorker: dispatchId is required');
|
|
318
436
|
if (_draining) throw new Error('agent-worker-pool: draining');
|
|
319
|
-
if (
|
|
437
|
+
if (!_enabled) throw new Error('agent-worker-pool: disabled');
|
|
438
|
+
if (_busy.has(dispatchId) || _leaseContexts.has(dispatchId) || _acquiring.has(dispatchId)) {
|
|
320
439
|
throw new Error(
|
|
321
440
|
`agent-worker-pool.acquireWorker: dispatchId ${dispatchId} already has an active worker ` +
|
|
322
441
|
'(1 worker : 1 active dispatch invariant — call release() before acquiring again)'
|
|
@@ -324,18 +443,43 @@ async function acquireWorker({
|
|
|
324
443
|
}
|
|
325
444
|
|
|
326
445
|
const mcpServersHash = hashMcpServers(mcpServers);
|
|
446
|
+
const runtimeAdapter = transport.resolveWorkerRuntime(runtime);
|
|
327
447
|
const workerProcessEnv = processEnv && typeof processEnv === 'object'
|
|
328
448
|
? Object.freeze({ ...processEnv })
|
|
329
449
|
: undefined;
|
|
330
|
-
const
|
|
450
|
+
const frozenWorkerOptions = Object.freeze(
|
|
451
|
+
workerOptions && typeof workerOptions === 'object' ? { ...workerOptions } : {}
|
|
452
|
+
);
|
|
453
|
+
const normalizedSessionCwd = path.resolve(cwd || process.cwd());
|
|
454
|
+
const normalizedProcessCwd = path.resolve(processCwd || cwd || process.cwd());
|
|
455
|
+
const processCwdKey = _pathKey(normalizedProcessCwd);
|
|
456
|
+
const workerArgs = buildWorkerArgs(runtimeAdapter, {
|
|
457
|
+
...frozenWorkerOptions,
|
|
458
|
+
cwd: normalizedProcessCwd,
|
|
459
|
+
env: workerProcessEnv,
|
|
460
|
+
});
|
|
461
|
+
const processContextHash = hashProcessContext(workerProcessEnv, {
|
|
462
|
+
runtime: runtimeAdapter.name,
|
|
463
|
+
workerArgs,
|
|
464
|
+
processCwd: processCwdKey,
|
|
465
|
+
});
|
|
331
466
|
const leaseContext = sessionContext && typeof sessionContext === 'object'
|
|
332
467
|
? { ...sessionContext }
|
|
333
468
|
: {};
|
|
334
469
|
const req = {
|
|
335
|
-
|
|
336
|
-
|
|
470
|
+
runtime: runtimeAdapter,
|
|
471
|
+
dispatchId, cwd: normalizedSessionCwd, processCwd: normalizedProcessCwd,
|
|
472
|
+
model, effort, mcpServers, mcpServersHash,
|
|
473
|
+
processEnv: workerProcessEnv, processContextHash, processCwdKey,
|
|
474
|
+
workerOptions: frozenWorkerOptions,
|
|
475
|
+
resumeSessionId: sessionId || null,
|
|
476
|
+
cancelled: false,
|
|
477
|
+
settled: false,
|
|
478
|
+
reserved: false,
|
|
479
|
+
worker: null,
|
|
337
480
|
};
|
|
338
481
|
_leaseContexts.set(dispatchId, leaseContext);
|
|
482
|
+
_acquiring.set(dispatchId, req);
|
|
339
483
|
|
|
340
484
|
let worker;
|
|
341
485
|
try {
|
|
@@ -347,30 +491,38 @@ async function acquireWorker({
|
|
|
347
491
|
});
|
|
348
492
|
|
|
349
493
|
if (_draining) throw new Error('agent-worker-pool: draining');
|
|
494
|
+
if (req.cancelled || !_enabled) throw new Error('agent-worker-pool: acquisition cancelled');
|
|
350
495
|
_busy.set(dispatchId, worker);
|
|
351
496
|
await _applyHermeticDirs(worker, hermeticDirs);
|
|
352
497
|
if (worker.killed) throw new Error('agent-worker-pool: worker died during acquisition');
|
|
353
|
-
if (
|
|
498
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
499
|
+
throw _acquisitionUnavailableError(req);
|
|
500
|
+
}
|
|
354
501
|
} catch (err) {
|
|
355
502
|
if (_busy.get(dispatchId) === worker) _busy.delete(dispatchId);
|
|
356
503
|
_clearLeaseContext(dispatchId);
|
|
357
504
|
if (worker) {
|
|
358
|
-
|
|
505
|
+
_closeWorker(worker);
|
|
359
506
|
_pump();
|
|
360
507
|
}
|
|
361
508
|
throw err;
|
|
509
|
+
} finally {
|
|
510
|
+
_acquiring.delete(dispatchId);
|
|
362
511
|
}
|
|
363
512
|
|
|
364
513
|
return {
|
|
365
514
|
dispatchId,
|
|
366
515
|
// P-1d8f0b93 — exposed so engine.js's PooledAgentProcess facade (the
|
|
367
516
|
// spawnAgent integration) can write the same PID-file/log identity a
|
|
368
|
-
// cold-spawned child_process would,
|
|
369
|
-
// synthesized `result` event
|
|
517
|
+
// cold-spawned child_process would, stamp the ACP sessionId into the
|
|
518
|
+
// synthesized `result` event, and expose the model selected by session/new.
|
|
519
|
+
// All are read-time
|
|
370
520
|
// snapshots of the underlying worker's current values (a getter, not a
|
|
371
521
|
// captured copy), since sessionId can rotate under a reused worker.
|
|
372
522
|
get pid() { return (worker.proc && worker.proc.pid) || null; },
|
|
523
|
+
get processStartedAt() { return worker.processStartedAt || 0; },
|
|
373
524
|
get sessionId() { return worker.sessionId || null; },
|
|
525
|
+
get currentModel() { return worker.currentModel || null; },
|
|
374
526
|
stream: async (promptText, opts = {}) => {
|
|
375
527
|
try {
|
|
376
528
|
return await worker.stream(promptText, {
|
|
@@ -386,9 +538,26 @@ async function acquireWorker({
|
|
|
386
538
|
return worker.cancel();
|
|
387
539
|
},
|
|
388
540
|
release: () => release(dispatchId),
|
|
541
|
+
discard: () => discard(dispatchId),
|
|
542
|
+
abandon: () => abandon(dispatchId),
|
|
543
|
+
preserveDescendants: (pids) => worker.preserveDescendants(pids),
|
|
389
544
|
};
|
|
390
545
|
}
|
|
391
546
|
|
|
547
|
+
function cancelAcquire(dispatchId) {
|
|
548
|
+
const req = _acquiring.get(dispatchId);
|
|
549
|
+
if (!req) return false;
|
|
550
|
+
req.cancelled = true;
|
|
551
|
+
const queueIdx = _queue.indexOf(req);
|
|
552
|
+
if (queueIdx >= 0) _queue.splice(queueIdx, 1);
|
|
553
|
+
if (_busy.get(dispatchId) === req.worker) _busy.delete(dispatchId);
|
|
554
|
+
_clearLeaseContext(dispatchId);
|
|
555
|
+
if (req.worker) _closeWorker(req.worker);
|
|
556
|
+
_settleRequest(req, 'reject', new Error('agent-worker-pool: acquisition cancelled'));
|
|
557
|
+
_pump();
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
|
|
392
561
|
// Return a leased worker to the free set — unless it died during the lease
|
|
393
562
|
// (WORKER_DIED), in which case it's simply dropped (already evicted from
|
|
394
563
|
// `_busy` by `_onWorkerExit` if the crash happened before release(), or
|
|
@@ -405,9 +574,18 @@ function release(dispatchId) {
|
|
|
405
574
|
_pump();
|
|
406
575
|
return true;
|
|
407
576
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
577
|
+
|
|
578
|
+
if (worker.inflight) {
|
|
579
|
+
_closeWorker(worker);
|
|
580
|
+
_trace(`dispatch=${dispatchId} release: worker=${worker.id} still had an inflight turn and was evicted`);
|
|
581
|
+
_pump();
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (_draining || !_enabled || _occupied() >= _poolSize) {
|
|
586
|
+
_closeWorker(worker);
|
|
587
|
+
_trace(`dispatch=${dispatchId} release: worker=${worker.id} closed instead of exceeding pool capacity`);
|
|
588
|
+
_pump();
|
|
411
589
|
return true;
|
|
412
590
|
}
|
|
413
591
|
_free.push(worker);
|
|
@@ -416,14 +594,55 @@ function release(dispatchId) {
|
|
|
416
594
|
return true;
|
|
417
595
|
}
|
|
418
596
|
|
|
597
|
+
function discard(dispatchId) {
|
|
598
|
+
const worker = _busy.get(dispatchId);
|
|
599
|
+
_clearLeaseContext(dispatchId);
|
|
600
|
+
if (!worker) return false;
|
|
601
|
+
_busy.delete(dispatchId);
|
|
602
|
+
_closeWorker(worker);
|
|
603
|
+
_trace(`dispatch=${dispatchId} discard: worker=${worker.id} evicted`);
|
|
604
|
+
_pump();
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function abandon(dispatchId) {
|
|
609
|
+
const worker = _busy.get(dispatchId);
|
|
610
|
+
_clearLeaseContext(dispatchId);
|
|
611
|
+
if (!worker) return false;
|
|
612
|
+
_busy.delete(dispatchId);
|
|
613
|
+
try { worker.abandon(); } catch { /* pipe already closed */ }
|
|
614
|
+
_trace(`dispatch=${dispatchId} abandon: worker=${worker.id} evicted without signaling an unverified PID`);
|
|
615
|
+
_pump();
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function setEnabled(enabled) {
|
|
620
|
+
const next = enabled === true;
|
|
621
|
+
if (_enabled === next) return _enabled;
|
|
622
|
+
if (next && _draining) return false;
|
|
623
|
+
_enabled = next;
|
|
624
|
+
if (!_enabled) {
|
|
625
|
+
for (const dispatchId of [..._acquiring.keys()]) cancelAcquire(dispatchId);
|
|
626
|
+
for (const worker of _free.splice(0)) _closeWorker(worker);
|
|
627
|
+
} else {
|
|
628
|
+
_pump();
|
|
629
|
+
}
|
|
630
|
+
return _enabled;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function isEnabled() {
|
|
634
|
+
return _enabled;
|
|
635
|
+
}
|
|
636
|
+
|
|
419
637
|
function beginDrain() {
|
|
420
638
|
if (_draining) return;
|
|
421
639
|
_draining = true;
|
|
422
640
|
for (const req of _queue.splice(0)) {
|
|
423
|
-
|
|
641
|
+
_clearLeaseContext(req.dispatchId);
|
|
642
|
+
_settleRequest(req, 'reject', new Error('agent-worker-pool: draining'));
|
|
424
643
|
}
|
|
425
644
|
for (const worker of _free.splice(0)) {
|
|
426
|
-
|
|
645
|
+
_closeWorker(worker);
|
|
427
646
|
}
|
|
428
647
|
}
|
|
429
648
|
|
|
@@ -441,26 +660,29 @@ function getActiveLeaseIds() {
|
|
|
441
660
|
|
|
442
661
|
function shutdown() {
|
|
443
662
|
_draining = true;
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
try { req.reject(new Error('agent-worker-pool: shutdown')); } catch { /* swallow */ }
|
|
447
|
-
}
|
|
663
|
+
_enabled = false;
|
|
664
|
+
for (const dispatchId of [..._acquiring.keys()]) cancelAcquire(dispatchId);
|
|
448
665
|
for (const worker of _free.splice(0)) {
|
|
449
|
-
|
|
666
|
+
_closeWorker(worker);
|
|
450
667
|
}
|
|
451
668
|
for (const [, worker] of _busy) {
|
|
452
|
-
|
|
669
|
+
_closeWorker(worker);
|
|
453
670
|
}
|
|
671
|
+
for (const worker of _starting) _closeWorker(worker);
|
|
454
672
|
_busy.clear();
|
|
455
673
|
for (const dispatchId of [..._leaseContexts.keys()]) _clearLeaseContext(dispatchId);
|
|
456
|
-
_reserved = 0;
|
|
457
674
|
}
|
|
458
675
|
|
|
459
676
|
module.exports = {
|
|
460
677
|
acquireWorker,
|
|
678
|
+
cancelAcquire,
|
|
461
679
|
release,
|
|
680
|
+
discard,
|
|
681
|
+
abandon,
|
|
462
682
|
setPoolSize,
|
|
463
683
|
getPoolSize,
|
|
684
|
+
setEnabled,
|
|
685
|
+
isEnabled,
|
|
464
686
|
beginDrain,
|
|
465
687
|
isDraining,
|
|
466
688
|
isDrained,
|
|
@@ -473,4 +695,6 @@ module.exports = {
|
|
|
473
695
|
_busy,
|
|
474
696
|
_queue,
|
|
475
697
|
_leaseContexts,
|
|
698
|
+
_acquiring,
|
|
699
|
+
_starting,
|
|
476
700
|
};
|
package/engine/cc-worker-pool.js
CHANGED
|
@@ -79,6 +79,7 @@ const {
|
|
|
79
79
|
const _internals = {
|
|
80
80
|
spawnAcp: transport.spawnAcp,
|
|
81
81
|
killImmediate: transport.killImmediate,
|
|
82
|
+
killRoot: transport.killRoot,
|
|
82
83
|
now() { return Date.now(); },
|
|
83
84
|
};
|
|
84
85
|
|
|
@@ -151,8 +152,9 @@ function _trace(...parts) {
|
|
|
151
152
|
|
|
152
153
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
153
154
|
|
|
154
|
-
async function getSession({ tabId, model, effort, mcpServers, systemPromptHash, cwd } = {}) {
|
|
155
|
+
async function getSession({ runtime, tabId, model, effort, mcpServers, systemPromptHash, cwd } = {}) {
|
|
155
156
|
if (!tabId) throw new Error('cc-worker-pool.getSession: tabId is required');
|
|
157
|
+
const runtimeAdapter = transport.resolveWorkerRuntime(runtime);
|
|
156
158
|
const mcpServersHash = _hashMcpServers(mcpServers);
|
|
157
159
|
let worker = _tabs.get(tabId);
|
|
158
160
|
// Track which lifecycle path we took so the dashboard's [cc-timing] log can
|
|
@@ -195,7 +197,7 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
195
197
|
if (worker.killed) {
|
|
196
198
|
_tabs.delete(tabId);
|
|
197
199
|
worker = null;
|
|
198
|
-
} else if (worker.mcpServersHash !== mcpServersHash) {
|
|
200
|
+
} else if (worker.runtime !== runtimeAdapter || worker.mcpServersHash !== mcpServersHash) {
|
|
199
201
|
// mcpServers shape changed → must respawn the proc; the daemon
|
|
200
202
|
// resolves MCP server config at process boot, not per session.
|
|
201
203
|
_tabs.delete(tabId);
|
|
@@ -206,7 +208,13 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
206
208
|
// and create a fresh one. Saves the ~2.1 s initialize handshake.
|
|
207
209
|
// Pass model/effort so the NEW session picks up any override the
|
|
208
210
|
// caller has rotated in (Bug B / issue #2479).
|
|
209
|
-
|
|
211
|
+
try {
|
|
212
|
+
await worker.newSession({ mcpServers, systemPromptHash, model, effort });
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (_tabs.get(tabId) === worker) _tabs.delete(tabId);
|
|
215
|
+
try { worker.close(); } catch { /* already torn down */ }
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
210
218
|
worker.lastUsedAt = _internals.now();
|
|
211
219
|
lifecycle = 'new-session';
|
|
212
220
|
} else {
|
|
@@ -222,6 +230,7 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
222
230
|
|
|
223
231
|
if (!worker) {
|
|
224
232
|
worker = new Worker({
|
|
233
|
+
runtime: runtimeAdapter,
|
|
225
234
|
id: tabId, model, effort, mcpServers, mcpServersHash, systemPromptHash, cwd,
|
|
226
235
|
internals: _internals, trace: _trace,
|
|
227
236
|
});
|
package/engine/cleanup.js
CHANGED
|
@@ -808,16 +808,20 @@ async function runCleanup(config, verbose = false) {
|
|
|
808
808
|
// normalized cwd values from managed-process state and protect any
|
|
809
809
|
// worktree dir that contains one. Cwd is optional per the schema, so
|
|
810
810
|
// entries without cwd contribute nothing.
|
|
811
|
-
const
|
|
811
|
+
const _anchoredProcessCwds = [];
|
|
812
812
|
try {
|
|
813
813
|
const _ms = require('./managed-spawn');
|
|
814
814
|
for (const rec of _ms.listManagedSpecs()) {
|
|
815
815
|
if (rec && typeof rec.cwd === 'string' && rec.cwd.length > 0) {
|
|
816
|
-
try {
|
|
816
|
+
try { _anchoredProcessCwds.push(shared.realPathForComparison(rec.cwd)); }
|
|
817
817
|
catch (_e) { /* malformed cwd — skip */ }
|
|
818
818
|
}
|
|
819
819
|
}
|
|
820
820
|
} catch (e) { log('warn', `managed-spawn cwd anchor lookup failed: ${e.message}`); }
|
|
821
|
+
try {
|
|
822
|
+
const _kp = require('./keep-process-sweep');
|
|
823
|
+
_anchoredProcessCwds.push(..._kp.getActiveKeepProcessCwds());
|
|
824
|
+
} catch (e) { log('warn', `keep-processes cwd anchor lookup failed: ${e.message}`); }
|
|
821
825
|
|
|
822
826
|
// Probe `git branch --show-current` for every worktree in chunks of 5.
|
|
823
827
|
// Sequential probing was the dominant cost in the cleanup phase
|
|
@@ -886,20 +890,20 @@ async function runCleanup(config, verbose = false) {
|
|
|
886
890
|
if (verbose) console.log(` Skipping worktree ${dir}: pool-borrowed by active dispatch`);
|
|
887
891
|
}
|
|
888
892
|
|
|
889
|
-
//
|
|
890
|
-
// Worktrees backing live
|
|
891
|
-
// originating dispatch
|
|
893
|
+
// Long-running process cwd anchor protection.
|
|
894
|
+
// Worktrees backing live managed_spawn or keep_processes cwds must outlive the
|
|
895
|
+
// originating dispatch.
|
|
892
896
|
// Overrides merged-branch and age sweeps because the cwd record is
|
|
893
897
|
// the authoritative signal that something live still uses the dir.
|
|
894
|
-
if (
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
if (
|
|
898
|
+
if (_anchoredProcessCwds.length > 0) {
|
|
899
|
+
for (const cwd of _anchoredProcessCwds) {
|
|
900
|
+
let anchored = false;
|
|
901
|
+
try { anchored = shared.isPathInsideOrEqual(cwd, wtPath); } catch {}
|
|
902
|
+
if (anchored) {
|
|
899
903
|
isProtected = true;
|
|
900
904
|
if (shouldClean) {
|
|
901
905
|
shouldClean = false;
|
|
902
|
-
if (verbose) console.log(` Skipping worktree ${dir}:
|
|
906
|
+
if (verbose) console.log(` Skipping worktree ${dir}: long-running process cwd anchor`);
|
|
903
907
|
}
|
|
904
908
|
break;
|
|
905
909
|
}
|