@yemi33/minions 0.1.2383 → 0.1.2385
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/dashboard/js/refresh.js +2 -1
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +133 -46
- 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 +7 -4
- package/docs/worktree-lifecycle.md +22 -0
- package/engine/acp-transport.js +216 -29
- package/engine/agent-worker-pool.js +268 -51
- package/engine/cc-worker-pool.js +8 -1
- package/engine/cleanup.js +15 -11
- package/engine/cli.js +47 -27
- package/engine/create-pr-worktree.js +57 -79
- package/engine/keep-process-sweep.js +131 -9
- package/engine/lifecycle.js +1 -1
- package/engine/live-checkout.js +4 -2
- package/engine/managed-spawn.js +112 -5
- package/engine/pooled-agent-process.js +486 -21
- package/engine/pr-action.js +6 -8
- package/engine/process-utils.js +154 -18
- package/engine/runtimes/copilot.js +22 -4
- package/engine/shared.js +254 -61
- package/engine/spawn-agent.js +6 -3
- package/engine/timeout.js +14 -10
- package/engine/worktree-gc.js +55 -46
- package/engine.js +237 -134
- package/package.json +1 -1
- package/playbooks/shared-rules.md +2 -2
- package/prompts/cc-system.md +11 -11
|
@@ -30,14 +30,15 @@
|
|
|
30
30
|
*
|
|
31
31
|
* Assignment strategy (acquireWorker), per the P-7a1c2e4f spike's findings:
|
|
32
32
|
*
|
|
33
|
-
* 1. A FREE worker whose
|
|
33
|
+
* 1. A FREE worker whose MCP config, process environment, and process cwd
|
|
34
|
+
* match the request wins
|
|
34
35
|
* first — reused via a fresh `session/new` (worker.newSession), no
|
|
35
36
|
* respawn, no wasted initialize handshake.
|
|
36
37
|
* 2. Otherwise any OTHER free worker is evicted (closed) and a replacement
|
|
37
38
|
* is spawned fresh with the requested mcpServers/cwd — MCP servers are
|
|
38
39
|
* resolved at `copilot --acp` process boot (via $COPILOT_HOME), so a
|
|
39
|
-
* changed local/stdio server set requires a full respawn,
|
|
40
|
-
* 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)).
|
|
41
42
|
* 3. Otherwise, if the pool has room (fewer than `poolSize` workers
|
|
42
43
|
* allocated across free+busy+in-flight-spawn), spawn a brand new
|
|
43
44
|
* worker.
|
|
@@ -59,7 +60,9 @@
|
|
|
59
60
|
* No new npm deps — Node built-ins only.
|
|
60
61
|
*/
|
|
61
62
|
|
|
63
|
+
const path = require('path');
|
|
62
64
|
const transport = require('./acp-transport');
|
|
65
|
+
const { buildWorkerArgs } = require('./runtimes');
|
|
63
66
|
const { Worker, hashMcpServers, hashProcessContext } = transport;
|
|
64
67
|
|
|
65
68
|
// Test seam — mirrors engine/cc-worker-pool.js's `_internals` pattern, but is
|
|
@@ -68,6 +71,11 @@ const { Worker, hashMcpServers, hashProcessContext } = transport;
|
|
|
68
71
|
const _internals = {
|
|
69
72
|
spawnAcp: transport.spawnAcp,
|
|
70
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
|
+
},
|
|
71
79
|
now() { return Date.now(); },
|
|
72
80
|
};
|
|
73
81
|
|
|
@@ -82,8 +90,10 @@ let _poolSize = DEFAULT_POOL_SIZE;
|
|
|
82
90
|
|
|
83
91
|
function setPoolSize(n) {
|
|
84
92
|
const parsed = Number(n);
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
const nextSize = Math.floor(parsed);
|
|
94
|
+
if (!Number.isFinite(parsed) || nextSize <= 0) return _poolSize;
|
|
95
|
+
_poolSize = nextSize;
|
|
96
|
+
_trimFreeWorkers();
|
|
87
97
|
_pump();
|
|
88
98
|
return _poolSize;
|
|
89
99
|
}
|
|
@@ -108,17 +118,57 @@ const _free = []; // Worker[] — idle, unleased
|
|
|
108
118
|
const _busy = new Map(); // dispatchId → Worker — currently leased
|
|
109
119
|
const _queue = []; // pending acquire requests, FIFO
|
|
110
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
|
|
111
123
|
// Slots reserved for a worker that's mid-reuse/respawn/fresh-spawn — pulled
|
|
112
124
|
// out of `_free` (or not yet spawned) but not yet placed in `_busy`. Counted
|
|
113
125
|
// toward `_poolSize` so a burst of concurrent acquireWorker calls can't
|
|
114
126
|
// overshoot the configured pool size while spawns are still in flight.
|
|
115
127
|
let _reserved = 0;
|
|
116
128
|
let _draining = false;
|
|
129
|
+
let _enabled = true;
|
|
117
130
|
|
|
118
131
|
function _occupied() {
|
|
119
132
|
return _free.length + _busy.size + _reserved;
|
|
120
133
|
}
|
|
121
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
|
+
|
|
122
172
|
// ── Queue pump ──────────────────────────────────────────────────────────
|
|
123
173
|
//
|
|
124
174
|
// Fully synchronous per pass: each iteration either satisfies the head of
|
|
@@ -130,18 +180,48 @@ function _occupied() {
|
|
|
130
180
|
// no-op re-pump; failure frees the reservation and DOES let another queued
|
|
131
181
|
// request through).
|
|
132
182
|
function _pump() {
|
|
183
|
+
if (_draining || !_enabled) return;
|
|
133
184
|
while (_queue.length > 0) {
|
|
134
185
|
const req = _queue[0];
|
|
135
186
|
|
|
136
|
-
const
|
|
187
|
+
const compatible = (w) => (
|
|
137
188
|
w.runtime === req.runtime
|
|
138
189
|
&& w.mcpServersHash === req.mcpServersHash
|
|
139
190
|
&& w.processContextHash === req.processContextHash
|
|
191
|
+
&& w.processCwdKey === req.processCwdKey
|
|
140
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);
|
|
141
220
|
if (matchIdx >= 0) {
|
|
142
221
|
_queue.shift();
|
|
143
222
|
const worker = _free.splice(matchIdx, 1)[0];
|
|
144
223
|
_reserved++;
|
|
224
|
+
req.reserved = true;
|
|
145
225
|
_reuseWorker(worker, req);
|
|
146
226
|
continue;
|
|
147
227
|
}
|
|
@@ -150,6 +230,7 @@ function _pump() {
|
|
|
150
230
|
_queue.shift();
|
|
151
231
|
const stale = _free.shift();
|
|
152
232
|
_reserved++;
|
|
233
|
+
req.reserved = true;
|
|
153
234
|
_respawnWorker(stale, req);
|
|
154
235
|
continue;
|
|
155
236
|
}
|
|
@@ -157,6 +238,7 @@ function _pump() {
|
|
|
157
238
|
if (_occupied() < _poolSize) {
|
|
158
239
|
_queue.shift();
|
|
159
240
|
_reserved++;
|
|
241
|
+
req.reserved = true;
|
|
160
242
|
_spawnFreshWorker(req);
|
|
161
243
|
continue;
|
|
162
244
|
}
|
|
@@ -168,40 +250,55 @@ function _pump() {
|
|
|
168
250
|
}
|
|
169
251
|
|
|
170
252
|
async function _reuseWorker(worker, req) {
|
|
253
|
+
req.worker = worker;
|
|
171
254
|
try {
|
|
172
|
-
|
|
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
|
+
}
|
|
173
270
|
_trace(`dispatch=${req.dispatchId} reuse: worker=${worker.id} sessionId=${worker.sessionId}`);
|
|
174
|
-
|
|
175
|
-
if (_draining) {
|
|
176
|
-
|
|
177
|
-
req
|
|
271
|
+
_releaseReservation(req);
|
|
272
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
273
|
+
_closeWorker(worker);
|
|
274
|
+
_settleRequest(req, 'reject', _acquisitionUnavailableError(req));
|
|
178
275
|
return;
|
|
179
276
|
}
|
|
180
|
-
req
|
|
277
|
+
_settleRequest(req, 'resolve', worker);
|
|
181
278
|
} catch (err) {
|
|
182
|
-
|
|
279
|
+
_releaseReservation(req);
|
|
183
280
|
// The worker died mid-newSession (or the daemon rejected the call) —
|
|
184
281
|
// it's already unusable; don't return it to `_free`.
|
|
185
|
-
|
|
186
|
-
req
|
|
282
|
+
_closeWorker(worker);
|
|
283
|
+
_settleRequest(req, 'reject', err);
|
|
187
284
|
_pump();
|
|
188
285
|
}
|
|
189
286
|
}
|
|
190
287
|
|
|
191
288
|
async function _respawnWorker(stale, req) {
|
|
192
|
-
|
|
289
|
+
_closeWorker(stale);
|
|
193
290
|
try {
|
|
194
291
|
const worker = await _bootWorker(req);
|
|
195
|
-
|
|
196
|
-
if (_draining) {
|
|
197
|
-
|
|
198
|
-
req
|
|
292
|
+
_releaseReservation(req);
|
|
293
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
294
|
+
_closeWorker(worker);
|
|
295
|
+
_settleRequest(req, 'reject', _acquisitionUnavailableError(req));
|
|
199
296
|
return;
|
|
200
297
|
}
|
|
201
|
-
req
|
|
298
|
+
_settleRequest(req, 'resolve', worker);
|
|
202
299
|
} catch (err) {
|
|
203
|
-
|
|
204
|
-
req
|
|
300
|
+
_releaseReservation(req);
|
|
301
|
+
_settleRequest(req, 'reject', err);
|
|
205
302
|
_pump();
|
|
206
303
|
}
|
|
207
304
|
}
|
|
@@ -209,16 +306,16 @@ async function _respawnWorker(stale, req) {
|
|
|
209
306
|
async function _spawnFreshWorker(req) {
|
|
210
307
|
try {
|
|
211
308
|
const worker = await _bootWorker(req);
|
|
212
|
-
|
|
213
|
-
if (_draining) {
|
|
214
|
-
|
|
215
|
-
req
|
|
309
|
+
_releaseReservation(req);
|
|
310
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
311
|
+
_closeWorker(worker);
|
|
312
|
+
_settleRequest(req, 'reject', _acquisitionUnavailableError(req));
|
|
216
313
|
return;
|
|
217
314
|
}
|
|
218
|
-
req
|
|
315
|
+
_settleRequest(req, 'resolve', worker);
|
|
219
316
|
} catch (err) {
|
|
220
|
-
|
|
221
|
-
req
|
|
317
|
+
_releaseReservation(req);
|
|
318
|
+
_settleRequest(req, 'reject', err);
|
|
222
319
|
_pump();
|
|
223
320
|
}
|
|
224
321
|
}
|
|
@@ -231,6 +328,9 @@ async function _spawnFreshWorker(req) {
|
|
|
231
328
|
// whichever pool collection it's currently sitting in, then re-pumps so a
|
|
232
329
|
// queued request can spawn its replacement.
|
|
233
330
|
async function _bootWorker(req) {
|
|
331
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
332
|
+
throw _acquisitionUnavailableError(req);
|
|
333
|
+
}
|
|
234
334
|
const worker = new Worker({
|
|
235
335
|
runtime: req.runtime,
|
|
236
336
|
id: `agent-pool-${_nextWorkerId++}`,
|
|
@@ -240,19 +340,33 @@ async function _bootWorker(req) {
|
|
|
240
340
|
mcpServersHash: req.mcpServersHash,
|
|
241
341
|
processEnv: req.processEnv,
|
|
242
342
|
processContextHash: req.processContextHash,
|
|
343
|
+
workerOptions: req.workerOptions,
|
|
344
|
+
resumeSessionId: req.resumeSessionId,
|
|
243
345
|
cwd: req.cwd,
|
|
346
|
+
processCwd: req.processCwd,
|
|
244
347
|
internals: _internals,
|
|
245
348
|
trace: _trace,
|
|
246
349
|
});
|
|
350
|
+
worker.processCwdKey = req.processCwdKey;
|
|
351
|
+
req.worker = worker;
|
|
352
|
+
_starting.add(worker);
|
|
247
353
|
const initPromise = worker._spawnAndInit();
|
|
354
|
+
const identityPromise = worker.proc?.pid
|
|
355
|
+
? _internals.getProcessStartedAt(worker.proc.pid)
|
|
356
|
+
: Promise.resolve(0);
|
|
248
357
|
worker.initPromise = initPromise;
|
|
249
358
|
try {
|
|
250
|
-
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;
|
|
251
364
|
} catch (err) {
|
|
252
|
-
|
|
365
|
+
_closeWorker(worker);
|
|
253
366
|
throw err;
|
|
254
367
|
} finally {
|
|
255
368
|
worker.initPromise = null;
|
|
369
|
+
_starting.delete(worker);
|
|
256
370
|
}
|
|
257
371
|
if (worker.proc) {
|
|
258
372
|
worker.proc.once('exit', () => _onWorkerExit(worker));
|
|
@@ -315,11 +429,13 @@ function _clearLeaseContext(dispatchId) {
|
|
|
315
429
|
}
|
|
316
430
|
|
|
317
431
|
async function acquireWorker({
|
|
318
|
-
runtime, dispatchId, cwd, model, effort, mcpServers, hermeticDirs,
|
|
432
|
+
runtime, dispatchId, cwd, processCwd, model, effort, mcpServers, hermeticDirs,
|
|
433
|
+
processEnv, workerOptions, sessionId, sessionContext,
|
|
319
434
|
} = {}) {
|
|
320
435
|
if (!dispatchId) throw new Error('agent-worker-pool.acquireWorker: dispatchId is required');
|
|
321
436
|
if (_draining) throw new Error('agent-worker-pool: draining');
|
|
322
|
-
if (
|
|
437
|
+
if (!_enabled) throw new Error('agent-worker-pool: disabled');
|
|
438
|
+
if (_busy.has(dispatchId) || _leaseContexts.has(dispatchId) || _acquiring.has(dispatchId)) {
|
|
323
439
|
throw new Error(
|
|
324
440
|
`agent-worker-pool.acquireWorker: dispatchId ${dispatchId} already has an active worker ` +
|
|
325
441
|
'(1 worker : 1 active dispatch invariant — call release() before acquiring again)'
|
|
@@ -331,16 +447,39 @@ async function acquireWorker({
|
|
|
331
447
|
const workerProcessEnv = processEnv && typeof processEnv === 'object'
|
|
332
448
|
? Object.freeze({ ...processEnv })
|
|
333
449
|
: undefined;
|
|
334
|
-
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
|
+
});
|
|
335
466
|
const leaseContext = sessionContext && typeof sessionContext === 'object'
|
|
336
467
|
? { ...sessionContext }
|
|
337
468
|
: {};
|
|
338
469
|
const req = {
|
|
339
470
|
runtime: runtimeAdapter,
|
|
340
|
-
dispatchId, cwd
|
|
341
|
-
|
|
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,
|
|
342
480
|
};
|
|
343
481
|
_leaseContexts.set(dispatchId, leaseContext);
|
|
482
|
+
_acquiring.set(dispatchId, req);
|
|
344
483
|
|
|
345
484
|
let worker;
|
|
346
485
|
try {
|
|
@@ -352,18 +491,23 @@ async function acquireWorker({
|
|
|
352
491
|
});
|
|
353
492
|
|
|
354
493
|
if (_draining) throw new Error('agent-worker-pool: draining');
|
|
494
|
+
if (req.cancelled || !_enabled) throw new Error('agent-worker-pool: acquisition cancelled');
|
|
355
495
|
_busy.set(dispatchId, worker);
|
|
356
496
|
await _applyHermeticDirs(worker, hermeticDirs);
|
|
357
497
|
if (worker.killed) throw new Error('agent-worker-pool: worker died during acquisition');
|
|
358
|
-
if (
|
|
498
|
+
if (req.cancelled || _draining || !_enabled) {
|
|
499
|
+
throw _acquisitionUnavailableError(req);
|
|
500
|
+
}
|
|
359
501
|
} catch (err) {
|
|
360
502
|
if (_busy.get(dispatchId) === worker) _busy.delete(dispatchId);
|
|
361
503
|
_clearLeaseContext(dispatchId);
|
|
362
504
|
if (worker) {
|
|
363
|
-
|
|
505
|
+
_closeWorker(worker);
|
|
364
506
|
_pump();
|
|
365
507
|
}
|
|
366
508
|
throw err;
|
|
509
|
+
} finally {
|
|
510
|
+
_acquiring.delete(dispatchId);
|
|
367
511
|
}
|
|
368
512
|
|
|
369
513
|
return {
|
|
@@ -376,6 +520,7 @@ async function acquireWorker({
|
|
|
376
520
|
// snapshots of the underlying worker's current values (a getter, not a
|
|
377
521
|
// captured copy), since sessionId can rotate under a reused worker.
|
|
378
522
|
get pid() { return (worker.proc && worker.proc.pid) || null; },
|
|
523
|
+
get processStartedAt() { return worker.processStartedAt || 0; },
|
|
379
524
|
get sessionId() { return worker.sessionId || null; },
|
|
380
525
|
get currentModel() { return worker.currentModel || null; },
|
|
381
526
|
stream: async (promptText, opts = {}) => {
|
|
@@ -393,9 +538,26 @@ async function acquireWorker({
|
|
|
393
538
|
return worker.cancel();
|
|
394
539
|
},
|
|
395
540
|
release: () => release(dispatchId),
|
|
541
|
+
discard: () => discard(dispatchId),
|
|
542
|
+
abandon: () => abandon(dispatchId),
|
|
543
|
+
preserveDescendants: (pids) => worker.preserveDescendants(pids),
|
|
396
544
|
};
|
|
397
545
|
}
|
|
398
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
|
+
|
|
399
561
|
// Return a leased worker to the free set — unless it died during the lease
|
|
400
562
|
// (WORKER_DIED), in which case it's simply dropped (already evicted from
|
|
401
563
|
// `_busy` by `_onWorkerExit` if the crash happened before release(), or
|
|
@@ -412,9 +574,18 @@ function release(dispatchId) {
|
|
|
412
574
|
_pump();
|
|
413
575
|
return true;
|
|
414
576
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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();
|
|
418
589
|
return true;
|
|
419
590
|
}
|
|
420
591
|
_free.push(worker);
|
|
@@ -423,14 +594,55 @@ function release(dispatchId) {
|
|
|
423
594
|
return true;
|
|
424
595
|
}
|
|
425
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
|
+
|
|
426
637
|
function beginDrain() {
|
|
427
638
|
if (_draining) return;
|
|
428
639
|
_draining = true;
|
|
429
640
|
for (const req of _queue.splice(0)) {
|
|
430
|
-
|
|
641
|
+
_clearLeaseContext(req.dispatchId);
|
|
642
|
+
_settleRequest(req, 'reject', new Error('agent-worker-pool: draining'));
|
|
431
643
|
}
|
|
432
644
|
for (const worker of _free.splice(0)) {
|
|
433
|
-
|
|
645
|
+
_closeWorker(worker);
|
|
434
646
|
}
|
|
435
647
|
}
|
|
436
648
|
|
|
@@ -448,26 +660,29 @@ function getActiveLeaseIds() {
|
|
|
448
660
|
|
|
449
661
|
function shutdown() {
|
|
450
662
|
_draining = true;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
try { req.reject(new Error('agent-worker-pool: shutdown')); } catch { /* swallow */ }
|
|
454
|
-
}
|
|
663
|
+
_enabled = false;
|
|
664
|
+
for (const dispatchId of [..._acquiring.keys()]) cancelAcquire(dispatchId);
|
|
455
665
|
for (const worker of _free.splice(0)) {
|
|
456
|
-
|
|
666
|
+
_closeWorker(worker);
|
|
457
667
|
}
|
|
458
668
|
for (const [, worker] of _busy) {
|
|
459
|
-
|
|
669
|
+
_closeWorker(worker);
|
|
460
670
|
}
|
|
671
|
+
for (const worker of _starting) _closeWorker(worker);
|
|
461
672
|
_busy.clear();
|
|
462
673
|
for (const dispatchId of [..._leaseContexts.keys()]) _clearLeaseContext(dispatchId);
|
|
463
|
-
_reserved = 0;
|
|
464
674
|
}
|
|
465
675
|
|
|
466
676
|
module.exports = {
|
|
467
677
|
acquireWorker,
|
|
678
|
+
cancelAcquire,
|
|
468
679
|
release,
|
|
680
|
+
discard,
|
|
681
|
+
abandon,
|
|
469
682
|
setPoolSize,
|
|
470
683
|
getPoolSize,
|
|
684
|
+
setEnabled,
|
|
685
|
+
isEnabled,
|
|
471
686
|
beginDrain,
|
|
472
687
|
isDraining,
|
|
473
688
|
isDrained,
|
|
@@ -480,4 +695,6 @@ module.exports = {
|
|
|
480
695
|
_busy,
|
|
481
696
|
_queue,
|
|
482
697
|
_leaseContexts,
|
|
698
|
+
_acquiring,
|
|
699
|
+
_starting,
|
|
483
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
|
|
|
@@ -207,7 +208,13 @@ async function getSession({ runtime, tabId, model, effort, mcpServers, systemPro
|
|
|
207
208
|
// and create a fresh one. Saves the ~2.1 s initialize handshake.
|
|
208
209
|
// Pass model/effort so the NEW session picks up any override the
|
|
209
210
|
// caller has rotated in (Bug B / issue #2479).
|
|
210
|
-
|
|
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
|
+
}
|
|
211
218
|
worker.lastUsedAt = _internals.now();
|
|
212
219
|
lifecycle = 'new-session';
|
|
213
220
|
} else {
|
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
|
}
|