@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
package/engine/acp-transport.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* engine/acp-transport.js — Shared ACP (Agent Client Protocol) transport layer
|
|
3
3
|
* (P-4c6e8a72, extracted from engine/cc-worker-pool.js).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* module owns
|
|
7
|
-
*
|
|
5
|
+
* ACP-capable harnesses expose a long-lived JSON-RPC server over stdin/stdout.
|
|
6
|
+
* This module owns protocol plumbing that is identical regardless of which
|
|
7
|
+
* adapter or consumer is using the worker:
|
|
8
8
|
*
|
|
9
|
-
* - process spawn (`spawnAcp`) —
|
|
10
|
-
* runtime adapter
|
|
9
|
+
* - process spawn (`spawnAcp`) — delegates binary and argv construction to
|
|
10
|
+
* the selected runtime adapter.
|
|
11
11
|
* - newline-delimited JSON-RPC framing, request/response correlation, and
|
|
12
12
|
* `session/update` notification dispatch (the `Worker` class).
|
|
13
13
|
* - typed error envelope (`ERROR_CODES` / `typedError`).
|
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
|
|
46
46
|
const { spawn } = require('child_process');
|
|
47
47
|
const crypto = require('crypto');
|
|
48
|
+
const { normalizeExecutionModel } = require('./execution-model');
|
|
49
|
+
const {
|
|
50
|
+
resolveRuntime,
|
|
51
|
+
resolveRuntimeByCapability,
|
|
52
|
+
buildWorkerArgs,
|
|
53
|
+
} = require('./runtimes');
|
|
48
54
|
|
|
49
55
|
// W-mpmwxni2000c25c7-c — typed error codes the transport emits through every
|
|
50
56
|
// failure exit so a consumer (CC streaming handler / doc-chat pool wrapper /
|
|
@@ -56,8 +62,7 @@ const ERROR_CODES = Object.freeze({
|
|
|
56
62
|
// because a transient PATH / fs glitch may recover.
|
|
57
63
|
WORKER_SPAWN_FAILED: 'worker-spawn-failed',
|
|
58
64
|
// The worker process exited DURING the ACP handshake (initialize or
|
|
59
|
-
// session/new)
|
|
60
|
-
// version is too old. Also fires when session/new returns no
|
|
65
|
+
// session/new), or session/new returned no
|
|
61
66
|
// sessionId. Retriable: the caller swaps to a fallback model / a re-auth
|
|
62
67
|
// may unblock the next attempt.
|
|
63
68
|
ACP_HANDSHAKE_FAILED: 'acp-handshake-failed',
|
|
@@ -88,26 +93,37 @@ function typedError(message, code, retriable = true) {
|
|
|
88
93
|
// consumers that pre-warm (like cc-worker-pool.js's warmTab) can share the
|
|
89
94
|
// same knob instead of hardcoding their own copy.
|
|
90
95
|
const WARM_MAX_CONCURRENT = 3;
|
|
96
|
+
const ACP_HANDSHAKE_TIMEOUT_MS = 60000;
|
|
97
|
+
const SESSION_LOAD_RETRY_ATTEMPTS = 10;
|
|
98
|
+
const SESSION_LOAD_RETRY_DELAY_MS = 100;
|
|
99
|
+
const SESSION_ROTATION_CANCEL_WAIT_MS = 1000;
|
|
100
|
+
|
|
101
|
+
function resolveWorkerRuntime(runtime) {
|
|
102
|
+
const adapter = typeof runtime === 'string'
|
|
103
|
+
? resolveRuntime(runtime)
|
|
104
|
+
: (runtime || resolveRuntimeByCapability('acpWorkerPool'));
|
|
105
|
+
if (adapter?.capabilities?.acpWorkerPool !== true) {
|
|
106
|
+
throw new Error(`Runtime "${adapter?.name || '<unknown>'}" does not support ACP workers`);
|
|
107
|
+
}
|
|
108
|
+
return adapter;
|
|
109
|
+
}
|
|
91
110
|
|
|
92
|
-
// Real spawn implementation
|
|
93
|
-
// so
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// npm shim on Windows — it ENOENTs instantly (copilot issue #2370 /
|
|
97
|
-
// W-mqid8uev). resolveBinary() resolves the real JS entry
|
|
98
|
-
// (node_modules/@github/copilot/index.js) and reports whether to spawn it
|
|
99
|
-
// directly (native) or under the current Node process (shim).
|
|
100
|
-
function spawnAcp({ cwd, env } = {}) {
|
|
101
|
-
const copilot = require('./runtimes/copilot');
|
|
111
|
+
// Real spawn implementation. Binary resolution and worker argv are both
|
|
112
|
+
// adapter-owned so harness CLI flag changes do not leak into the transport.
|
|
113
|
+
function spawnAcp({ runtime, cwd, env, workerOptions } = {}) {
|
|
114
|
+
const adapter = resolveWorkerRuntime(runtime);
|
|
102
115
|
const workerEnv = env || process.env;
|
|
103
|
-
const resolved =
|
|
116
|
+
const resolved = adapter.resolveBinary({ env: workerEnv });
|
|
104
117
|
if (!resolved || !resolved.bin) {
|
|
105
118
|
throw new Error(
|
|
106
|
-
|
|
119
|
+
`${adapter.name} binary not resolvable -- ${adapter.installHint || `install the ${adapter.name} CLI`}`
|
|
107
120
|
);
|
|
108
121
|
}
|
|
109
122
|
const { bin, native, leadingArgs = [] } = resolved;
|
|
110
|
-
const acpArgs = [
|
|
123
|
+
const acpArgs = [
|
|
124
|
+
...leadingArgs,
|
|
125
|
+
...buildWorkerArgs(adapter, { ...(workerOptions || {}), cwd, env: workerEnv }),
|
|
126
|
+
];
|
|
111
127
|
// Mirror engine/spawn-agent.js: native binaries run directly; a non-native
|
|
112
128
|
// (JS entry / npm shim) runs under process.execPath as `node <bin> ...`.
|
|
113
129
|
const execBin = native ? bin : process.execPath;
|
|
@@ -134,6 +150,10 @@ function killImmediate(proc) {
|
|
|
134
150
|
}
|
|
135
151
|
}
|
|
136
152
|
|
|
153
|
+
function killRoot(proc) {
|
|
154
|
+
try { proc.kill('SIGKILL'); } catch { /* already dead */ }
|
|
155
|
+
}
|
|
156
|
+
|
|
137
157
|
function hashMcpServers(mcpServers) {
|
|
138
158
|
// Stable hash via JSON.stringify; mcpServers is an array of plain objects
|
|
139
159
|
// in practice (name/command/env) so the natural key order is fine.
|
|
@@ -141,18 +161,31 @@ function hashMcpServers(mcpServers) {
|
|
|
141
161
|
return crypto.createHash('sha256').update(json).digest('hex');
|
|
142
162
|
}
|
|
143
163
|
|
|
144
|
-
function
|
|
145
|
-
if (
|
|
164
|
+
function _stableValue(value) {
|
|
165
|
+
if (Array.isArray(value)) return value.map(_stableValue);
|
|
166
|
+
if (!value || typeof value !== 'object') return value;
|
|
146
167
|
return Object.fromEntries(
|
|
147
168
|
Object.entries(value)
|
|
148
169
|
.filter(([, entry]) => entry !== undefined)
|
|
149
170
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
171
|
+
.map(([key, entry]) => [key, _stableValue(entry)])
|
|
150
172
|
);
|
|
151
173
|
}
|
|
152
174
|
|
|
153
|
-
function
|
|
175
|
+
function _stableObject(value) {
|
|
176
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
177
|
+
return _stableValue(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hashProcessContext(processEnv, processOptions) {
|
|
181
|
+
const context = processOptions === undefined
|
|
182
|
+
? _stableObject(processEnv)
|
|
183
|
+
: {
|
|
184
|
+
env: _stableObject(processEnv),
|
|
185
|
+
options: _stableValue(processOptions),
|
|
186
|
+
};
|
|
154
187
|
return crypto.createHash('sha256')
|
|
155
|
-
.update(JSON.stringify(
|
|
188
|
+
.update(JSON.stringify(context))
|
|
156
189
|
.digest('hex');
|
|
157
190
|
}
|
|
158
191
|
|
|
@@ -181,6 +214,23 @@ function buildSessionNewParams({ cwd, mcpServers, model, effort }) {
|
|
|
181
214
|
return params;
|
|
182
215
|
}
|
|
183
216
|
|
|
217
|
+
function extractSessionModel(result) {
|
|
218
|
+
for (const value of [
|
|
219
|
+
result?.models?.currentModelId,
|
|
220
|
+
result?.currentModelId,
|
|
221
|
+
result?.modelId,
|
|
222
|
+
result?.model,
|
|
223
|
+
]) {
|
|
224
|
+
const model = normalizeExecutionModel(value);
|
|
225
|
+
if (model) return model;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildSessionLoadParams({ sessionId, cwd, mcpServers }) {
|
|
231
|
+
return { sessionId, cwd, mcpServers: mcpServers || [] };
|
|
232
|
+
}
|
|
233
|
+
|
|
184
234
|
function extractChunkText(content) {
|
|
185
235
|
if (content == null) return '';
|
|
186
236
|
if (typeof content === 'string') return content;
|
|
@@ -246,9 +296,11 @@ function mapAcpToolCallToToolUse(update) {
|
|
|
246
296
|
*/
|
|
247
297
|
class Worker {
|
|
248
298
|
constructor({
|
|
299
|
+
runtime,
|
|
249
300
|
id, model, effort, mcpServers, mcpServersHash, processEnv, processContextHash,
|
|
250
|
-
systemPromptHash, cwd, internals, trace,
|
|
301
|
+
workerOptions, resumeSessionId, systemPromptHash, cwd, processCwd, internals, trace,
|
|
251
302
|
}) {
|
|
303
|
+
this.runtime = resolveWorkerRuntime(runtime);
|
|
252
304
|
this.id = id;
|
|
253
305
|
this.model = model;
|
|
254
306
|
this.effort = effort;
|
|
@@ -256,12 +308,16 @@ class Worker {
|
|
|
256
308
|
this.mcpServersHash = mcpServersHash;
|
|
257
309
|
this.processEnv = processEnv;
|
|
258
310
|
this.processContextHash = processContextHash;
|
|
311
|
+
this.workerOptions = workerOptions && typeof workerOptions === 'object' ? { ...workerOptions } : {};
|
|
312
|
+
this.resumeSessionId = resumeSessionId || null;
|
|
259
313
|
this.systemPromptHash = systemPromptHash;
|
|
260
314
|
this.cwd = cwd || process.cwd();
|
|
315
|
+
this.processCwd = processCwd || this.cwd;
|
|
261
316
|
this._internals = internals;
|
|
262
317
|
this._trace = typeof trace === 'function' ? trace : () => {};
|
|
263
318
|
|
|
264
319
|
this.proc = null;
|
|
320
|
+
this.processStartedAt = 0;
|
|
265
321
|
this.sessionId = null;
|
|
266
322
|
this.pending = new Map(); // id → { resolve, reject }
|
|
267
323
|
this.inflight = null; // current { id, sessionId, onChunk, onDone, onError, signal, signalHandler, settled }
|
|
@@ -271,6 +327,7 @@ class Worker {
|
|
|
271
327
|
this.killed = false;
|
|
272
328
|
this.spawnError = null;
|
|
273
329
|
this.firstSystemPromptSent = false;
|
|
330
|
+
this.preservedDescendantPids = new Set();
|
|
274
331
|
// In-flight spawn+initialize+session/new promise. Set by the consumer
|
|
275
332
|
// before the worker is registered in its own pool bookkeeping, cleared
|
|
276
333
|
// after the handshake settles. Racing callers await this to avoid the
|
|
@@ -283,13 +340,16 @@ class Worker {
|
|
|
283
340
|
async _spawnAndInit() {
|
|
284
341
|
let proc;
|
|
285
342
|
try {
|
|
286
|
-
proc = this._internals.spawnAcp({
|
|
343
|
+
proc = this._internals.spawnAcp({
|
|
344
|
+
runtime: this.runtime,
|
|
345
|
+
cwd: this.processCwd,
|
|
346
|
+
env: this.processEnv,
|
|
347
|
+
workerOptions: this.workerOptions,
|
|
348
|
+
});
|
|
287
349
|
} catch (err) {
|
|
288
|
-
|
|
289
|
-
// on PATH) or EACCES. Surface as worker-spawn-failed so the consumer
|
|
290
|
-
// can show "install the CLI / fix PATH" guidance.
|
|
350
|
+
const hint = this.runtime.workerErrorHint || `${this.runtime.name} persistent worker failed`;
|
|
291
351
|
throw typedError(
|
|
292
|
-
|
|
352
|
+
`${hint} (${err.message})`,
|
|
293
353
|
ERROR_CODES.WORKER_SPAWN_FAILED,
|
|
294
354
|
true
|
|
295
355
|
);
|
|
@@ -307,11 +367,9 @@ class Worker {
|
|
|
307
367
|
const earlyExitPromise = new Promise((_, reject) => {
|
|
308
368
|
earlyExitReject = (code) => {
|
|
309
369
|
this.killed = true;
|
|
310
|
-
|
|
311
|
-
// always missing `copilot login`, stale CLI, or daemon crash on
|
|
312
|
-
// boot). Retriable so re-auth or a CLI upgrade can recover.
|
|
370
|
+
const hint = this.runtime.workerErrorHint || `${this.runtime.name} persistent worker failed`;
|
|
313
371
|
const err = typedError(
|
|
314
|
-
|
|
372
|
+
`${hint} (exit ${code})`,
|
|
315
373
|
ERROR_CODES.ACP_HANDSHAKE_FAILED,
|
|
316
374
|
true
|
|
317
375
|
);
|
|
@@ -327,8 +385,9 @@ class Worker {
|
|
|
327
385
|
// proc 'error' event fires when the OS can't actually start the child
|
|
328
386
|
// (ENOENT after a successful spawn() call, etc.). Treat as a spawn
|
|
329
387
|
// failure even though we made it past the synchronous spawn() above.
|
|
388
|
+
const hint = this.runtime.workerErrorHint || `${this.runtime.name} persistent worker failed`;
|
|
330
389
|
const wrapped = typedError(
|
|
331
|
-
|
|
390
|
+
`${hint} (${err.message})`,
|
|
332
391
|
ERROR_CODES.WORKER_SPAWN_FAILED,
|
|
333
392
|
true
|
|
334
393
|
);
|
|
@@ -338,28 +397,63 @@ class Worker {
|
|
|
338
397
|
};
|
|
339
398
|
proc.on('error', errorHandler);
|
|
340
399
|
|
|
400
|
+
const configuredHandshakeTimeout = Number(this._internals.handshakeTimeoutMs);
|
|
401
|
+
const handshakeTimeoutMs = Number.isFinite(configuredHandshakeTimeout) && configuredHandshakeTimeout > 0
|
|
402
|
+
? configuredHandshakeTimeout
|
|
403
|
+
: ACP_HANDSHAKE_TIMEOUT_MS;
|
|
404
|
+
let handshakeTimer;
|
|
405
|
+
const handshakeTimeoutPromise = new Promise((_, reject) => {
|
|
406
|
+
handshakeTimer = setTimeout(() => {
|
|
407
|
+
reject(typedError(
|
|
408
|
+
`copilot --acp handshake timed out after ${handshakeTimeoutMs}ms`,
|
|
409
|
+
ERROR_CODES.ACP_HANDSHAKE_FAILED,
|
|
410
|
+
true
|
|
411
|
+
));
|
|
412
|
+
}, handshakeTimeoutMs);
|
|
413
|
+
});
|
|
414
|
+
|
|
341
415
|
try {
|
|
342
416
|
await Promise.race([
|
|
343
417
|
this._call('initialize', { protocolVersion: 1, clientCapabilities: {} }),
|
|
344
418
|
earlyExitPromise,
|
|
419
|
+
handshakeTimeoutPromise,
|
|
345
420
|
]);
|
|
421
|
+
const sessionMethod = this.resumeSessionId ? 'session/load' : 'session/new';
|
|
422
|
+
const sessionParams = this.resumeSessionId
|
|
423
|
+
? buildSessionLoadParams({
|
|
424
|
+
sessionId: this.resumeSessionId,
|
|
425
|
+
cwd: this.cwd,
|
|
426
|
+
mcpServers: this.mcpServers,
|
|
427
|
+
})
|
|
428
|
+
: buildSessionNewParams({
|
|
429
|
+
cwd: this.cwd,
|
|
430
|
+
mcpServers: this.mcpServers,
|
|
431
|
+
model: this.model,
|
|
432
|
+
effort: this.effort,
|
|
433
|
+
});
|
|
434
|
+
const sessionPromise = sessionMethod === 'session/load'
|
|
435
|
+
? this._loadSession(sessionParams)
|
|
436
|
+
: this._call(sessionMethod, sessionParams);
|
|
346
437
|
const result = await Promise.race([
|
|
347
|
-
|
|
348
|
-
cwd: this.cwd, mcpServers: this.mcpServers, model: this.model, effort: this.effort,
|
|
349
|
-
})),
|
|
438
|
+
sessionPromise,
|
|
350
439
|
earlyExitPromise,
|
|
440
|
+
handshakeTimeoutPromise,
|
|
351
441
|
]);
|
|
352
|
-
this.sessionId = result && result.sessionId;
|
|
442
|
+
this.sessionId = (result && result.sessionId) || this.resumeSessionId;
|
|
443
|
+
this.currentModel = extractSessionModel(result) || normalizeExecutionModel(this.model);
|
|
353
444
|
if (!this.sessionId) {
|
|
354
445
|
// Handshake completed without an error but the daemon didn't hand
|
|
355
446
|
// back a sessionId — protocol violation or partial init failure.
|
|
447
|
+
const hint = this.runtime.workerErrorHint || `${this.runtime.name} persistent worker failed`;
|
|
356
448
|
throw typedError(
|
|
357
|
-
|
|
449
|
+
`${hint} (${sessionMethod} returned no sessionId)`,
|
|
358
450
|
ERROR_CODES.ACP_HANDSHAKE_FAILED,
|
|
359
451
|
true
|
|
360
452
|
);
|
|
361
453
|
}
|
|
454
|
+
this.firstSystemPromptSent = !!this.resumeSessionId;
|
|
362
455
|
} finally {
|
|
456
|
+
clearTimeout(handshakeTimer);
|
|
363
457
|
// Either the handshake finished (swap to a persistent exit handler that
|
|
364
458
|
// just marks killed) or it failed (proc is dying anyway).
|
|
365
459
|
proc.removeListener('exit', earlyExitHandler);
|
|
@@ -369,7 +463,7 @@ class Worker {
|
|
|
369
463
|
// Post-handshake exit = the daemon died mid-conversation. Retriable
|
|
370
464
|
// because the next call will cold-spawn a fresh worker.
|
|
371
465
|
const err = typedError(
|
|
372
|
-
|
|
466
|
+
`${this.runtime.name} persistent worker process exited`,
|
|
373
467
|
ERROR_CODES.WORKER_DIED,
|
|
374
468
|
true
|
|
375
469
|
);
|
|
@@ -408,9 +502,13 @@ class Worker {
|
|
|
408
502
|
_handleMessage(obj) {
|
|
409
503
|
// Response (matches an outbound id)
|
|
410
504
|
if (obj.id != null && this.pending.has(obj.id)) {
|
|
411
|
-
const { resolve, reject } = this.pending.get(obj.id);
|
|
505
|
+
const { resolve, reject, method } = this.pending.get(obj.id);
|
|
412
506
|
this.pending.delete(obj.id);
|
|
413
|
-
if (obj.error)
|
|
507
|
+
if (obj.error) {
|
|
508
|
+
const err = new Error(obj.error.message || 'ACP error');
|
|
509
|
+
err.acpMethod = method || null;
|
|
510
|
+
reject(err);
|
|
511
|
+
}
|
|
414
512
|
else resolve(obj.result);
|
|
415
513
|
return;
|
|
416
514
|
}
|
|
@@ -460,10 +558,35 @@ class Worker {
|
|
|
460
558
|
catch { /* pipe may be broken; exit handler will fire */ }
|
|
461
559
|
}
|
|
462
560
|
|
|
463
|
-
_call(method, params) {
|
|
561
|
+
_call(method, params, timeoutMs) {
|
|
562
|
+
if (this.killed || !this.proc?.stdin || this.proc.stdin.destroyed) {
|
|
563
|
+
return Promise.reject(new Error('acp-transport: worker is closed'));
|
|
564
|
+
}
|
|
464
565
|
return new Promise((resolve, reject) => {
|
|
465
566
|
const id = this.nextReqId++;
|
|
466
|
-
|
|
567
|
+
let timer;
|
|
568
|
+
const settle = (callback, value) => {
|
|
569
|
+
if (timer) clearTimeout(timer);
|
|
570
|
+
callback(value);
|
|
571
|
+
};
|
|
572
|
+
this.pending.set(id, {
|
|
573
|
+
resolve: (value) => settle(resolve, value),
|
|
574
|
+
reject: (err) => settle(reject, err),
|
|
575
|
+
method,
|
|
576
|
+
});
|
|
577
|
+
const parsedTimeout = Number(timeoutMs);
|
|
578
|
+
if (Number.isFinite(parsedTimeout) && parsedTimeout > 0) {
|
|
579
|
+
timer = setTimeout(() => {
|
|
580
|
+
if (!this.pending.delete(id)) return;
|
|
581
|
+
const err = typedError(
|
|
582
|
+
`acp-transport: ${method} timed out after ${parsedTimeout}ms`,
|
|
583
|
+
ERROR_CODES.ACP_HANDSHAKE_FAILED,
|
|
584
|
+
true
|
|
585
|
+
);
|
|
586
|
+
err.acpMethod = method;
|
|
587
|
+
reject(err);
|
|
588
|
+
}, parsedTimeout);
|
|
589
|
+
}
|
|
467
590
|
this._writeFrame({ jsonrpc: '2.0', id, method, params });
|
|
468
591
|
});
|
|
469
592
|
}
|
|
@@ -472,6 +595,20 @@ class Worker {
|
|
|
472
595
|
this._writeFrame({ jsonrpc: '2.0', method, params });
|
|
473
596
|
}
|
|
474
597
|
|
|
598
|
+
async _loadSession(params) {
|
|
599
|
+
let lastError;
|
|
600
|
+
for (let attempt = 0; attempt < SESSION_LOAD_RETRY_ATTEMPTS; attempt++) {
|
|
601
|
+
try {
|
|
602
|
+
return await this._call('session/load', params, ACP_HANDSHAKE_TIMEOUT_MS);
|
|
603
|
+
} catch (err) {
|
|
604
|
+
lastError = err;
|
|
605
|
+
if (!/already loaded/i.test(String(err && err.message))) throw err;
|
|
606
|
+
await new Promise((resolve) => setTimeout(resolve, SESSION_LOAD_RETRY_DELAY_MS));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
throw lastError;
|
|
610
|
+
}
|
|
611
|
+
|
|
475
612
|
// ── Stream a single turn ───────────────────────────────────────────────
|
|
476
613
|
stream(promptText, opts = {}) {
|
|
477
614
|
const {
|
|
@@ -496,8 +633,11 @@ class Worker {
|
|
|
496
633
|
// message remains correct.
|
|
497
634
|
let prompt = promptText;
|
|
498
635
|
const trustedContextText = buildTrustedSessionContext(trustedSessionContext);
|
|
499
|
-
const trustedSystemText = [
|
|
500
|
-
|
|
636
|
+
const trustedSystemText = [
|
|
637
|
+
!this.firstSystemPromptSent ? systemPromptText : null,
|
|
638
|
+
trustedContextText,
|
|
639
|
+
].filter(Boolean).join('\n\n');
|
|
640
|
+
if (trustedSystemText) {
|
|
501
641
|
prompt = `<system>\n${trustedSystemText}\n</system>\n\n${promptText}`;
|
|
502
642
|
}
|
|
503
643
|
if (!deferSystemPrompt) this.firstSystemPromptSent = true;
|
|
@@ -563,7 +703,8 @@ class Worker {
|
|
|
563
703
|
this._notify('session/cancel', { sessionId: this.inflight.sessionId });
|
|
564
704
|
}
|
|
565
705
|
|
|
566
|
-
async
|
|
706
|
+
async _prepareSession(options = {}) {
|
|
707
|
+
const { sessionId, mcpServers, systemPromptHash, model, effort, cwd } = options;
|
|
567
708
|
// Cancel any inflight before swapping the underlying session.
|
|
568
709
|
if (this.inflight) {
|
|
569
710
|
this.cancel();
|
|
@@ -573,28 +714,91 @@ class Worker {
|
|
|
573
714
|
await new Promise((resolve) => {
|
|
574
715
|
const start = this._internals.now();
|
|
575
716
|
const wait = () => {
|
|
576
|
-
if (!inflight || inflight.settled
|
|
717
|
+
if (!inflight || inflight.settled
|
|
718
|
+
|| this._internals.now() - start > SESSION_ROTATION_CANCEL_WAIT_MS) resolve();
|
|
577
719
|
else setTimeout(wait, 5);
|
|
578
720
|
};
|
|
579
721
|
wait();
|
|
580
722
|
});
|
|
723
|
+
if (!inflight.settled) {
|
|
724
|
+
throw new Error('acp-transport: cancelled prompt did not settle before session rotation');
|
|
725
|
+
}
|
|
581
726
|
}
|
|
582
727
|
// Bug B (issue #2479): if the caller is rotating the session because the
|
|
583
728
|
// system prompt changed, they may also be passing a fresh model/effort —
|
|
584
729
|
// update bookkeeping BEFORE session/new so the new fields land on the
|
|
585
|
-
// daemon.
|
|
586
|
-
|
|
730
|
+
// daemon. An explicit `model: undefined` clears a prior pooled override and
|
|
731
|
+
// lets the new session select its own default.
|
|
732
|
+
if (Object.prototype.hasOwnProperty.call(options, 'model')) this.model = model;
|
|
587
733
|
if (effort !== undefined) this.effort = effort;
|
|
588
734
|
// agent-worker-pool.js reuses a warm worker across dispatches with
|
|
589
735
|
// different cwds (unlike cc-worker-pool.js, whose tabs never change
|
|
590
736
|
// cwd) — allow the caller to rotate it in on a session swap.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
737
|
+
const nextCwd = cwd !== undefined && cwd !== null && cwd !== '' ? cwd : this.cwd;
|
|
738
|
+
if (sessionId && this.sessionId === sessionId) {
|
|
739
|
+
if (nextCwd !== this.cwd) {
|
|
740
|
+
const err = new Error('acp-transport: loaded session cannot change cwd without a worker respawn');
|
|
741
|
+
err.acpMethod = 'session/load';
|
|
742
|
+
throw err;
|
|
743
|
+
}
|
|
744
|
+
this.systemPromptHash = systemPromptHash;
|
|
745
|
+
this.firstSystemPromptSent = true;
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
this.cwd = nextCwd;
|
|
749
|
+
const method = sessionId ? 'session/load' : 'session/new';
|
|
750
|
+
const params = sessionId
|
|
751
|
+
? buildSessionLoadParams({ sessionId, cwd: this.cwd, mcpServers: mcpServers || [] })
|
|
752
|
+
: buildSessionNewParams({
|
|
753
|
+
cwd: this.cwd,
|
|
754
|
+
mcpServers: mcpServers || [],
|
|
755
|
+
model: this.model,
|
|
756
|
+
effort: this.effort,
|
|
757
|
+
});
|
|
758
|
+
const result = method === 'session/load'
|
|
759
|
+
? await this._loadSession(params)
|
|
760
|
+
: await this._call(method, params, ACP_HANDSHAKE_TIMEOUT_MS);
|
|
761
|
+
this.sessionId = (result && result.sessionId) || sessionId;
|
|
762
|
+
if (!this.sessionId) {
|
|
763
|
+
const hint = this.runtime.workerErrorHint || `${this.runtime.name} persistent worker failed`;
|
|
764
|
+
throw typedError(
|
|
765
|
+
`${hint} (${method} returned no sessionId)`,
|
|
766
|
+
ERROR_CODES.ACP_HANDSHAKE_FAILED,
|
|
767
|
+
true
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
this.currentModel = extractSessionModel(result) || normalizeExecutionModel(this.model);
|
|
596
771
|
this.systemPromptHash = systemPromptHash;
|
|
597
|
-
this.firstSystemPromptSent =
|
|
772
|
+
this.firstSystemPromptSent = !!sessionId;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async newSession(opts) {
|
|
776
|
+
return this._prepareSession({ ...opts, sessionId: null });
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async loadSession(sessionId, opts) {
|
|
780
|
+
return this._prepareSession({ ...opts, sessionId });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
preserveDescendants(pids) {
|
|
784
|
+
for (const pid of pids || []) {
|
|
785
|
+
const value = Number(pid);
|
|
786
|
+
if (Number.isInteger(value) && value > 0) this.preservedDescendantPids.add(value);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
abandon() {
|
|
791
|
+
if (this.killed) return;
|
|
792
|
+
this.killed = true;
|
|
793
|
+
try { this.proc?.stdin?.end(); } catch { /* pipe already closed */ }
|
|
794
|
+
const err = new Error('acp-transport: unverified worker abandoned');
|
|
795
|
+
this._failAllPending(err);
|
|
796
|
+
if (this.inflight && !this.inflight.settled) {
|
|
797
|
+
const cb = this.inflight.onError;
|
|
798
|
+
this.inflight.settled = true;
|
|
799
|
+
this.inflight = null;
|
|
800
|
+
if (cb) try { cb(err); } catch { /* swallow */ }
|
|
801
|
+
}
|
|
598
802
|
}
|
|
599
803
|
|
|
600
804
|
close() {
|
|
@@ -607,7 +811,13 @@ class Worker {
|
|
|
607
811
|
catch { /* swallow */ }
|
|
608
812
|
}
|
|
609
813
|
if (this.proc) {
|
|
610
|
-
try {
|
|
814
|
+
try {
|
|
815
|
+
if (this.preservedDescendantPids.size > 0 && typeof this._internals.killRoot === 'function') {
|
|
816
|
+
this._internals.killRoot(this.proc);
|
|
817
|
+
} else {
|
|
818
|
+
this._internals.killImmediate(this.proc);
|
|
819
|
+
}
|
|
820
|
+
} catch { /* already dead */ }
|
|
611
821
|
}
|
|
612
822
|
this._failAllPending(new Error('acp-transport: worker closed'));
|
|
613
823
|
if (this.inflight && !this.inflight.settled) {
|
|
@@ -623,13 +833,18 @@ module.exports = {
|
|
|
623
833
|
ERROR_CODES,
|
|
624
834
|
typedError,
|
|
625
835
|
spawnAcp,
|
|
836
|
+
resolveWorkerRuntime,
|
|
626
837
|
killImmediate,
|
|
838
|
+
killRoot,
|
|
627
839
|
hashMcpServers,
|
|
628
840
|
hashProcessContext,
|
|
629
841
|
buildTrustedSessionContext,
|
|
630
842
|
buildSessionNewParams,
|
|
843
|
+
extractSessionModel,
|
|
844
|
+
buildSessionLoadParams,
|
|
631
845
|
extractChunkText,
|
|
632
846
|
mapAcpToolCallToToolUse,
|
|
633
847
|
Worker,
|
|
634
848
|
WARM_MAX_CONCURRENT,
|
|
849
|
+
ACP_HANDSHAKE_TIMEOUT_MS,
|
|
635
850
|
};
|
package/engine/ado-comment.js
CHANGED
|
@@ -106,6 +106,7 @@ async function _defaultAcquireToken() {
|
|
|
106
106
|
* @param {string} args.agentId minions agent id (marker)
|
|
107
107
|
* @param {string} args.kind comment kind (marker)
|
|
108
108
|
* @param {string} [args.workItemId] originating work-item id (marker)
|
|
109
|
+
* @param {string} [args.model] concrete runtime execution model
|
|
109
110
|
* @param {number} [args.timeoutMs=30000]
|
|
110
111
|
* @param {Function} [args.acquireToken] () => Promise<string> — injectable token source
|
|
111
112
|
* @param {Function} [args.fetchImpl=fetch] injectable fetch
|
|
@@ -120,6 +121,7 @@ async function postAdoPrComment({
|
|
|
120
121
|
agentId,
|
|
121
122
|
kind,
|
|
122
123
|
workItemId,
|
|
124
|
+
model,
|
|
123
125
|
resolved = false,
|
|
124
126
|
timeoutMs = 30000,
|
|
125
127
|
acquireToken = _defaultAcquireToken,
|
|
@@ -131,7 +133,7 @@ async function postAdoPrComment({
|
|
|
131
133
|
_validatePrNumber(prNumber);
|
|
132
134
|
|
|
133
135
|
// buildMinionsCommentBody validates marker fields and throws on bad input.
|
|
134
|
-
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
|
|
136
|
+
const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, model, body });
|
|
135
137
|
|
|
136
138
|
const token = await acquireToken();
|
|
137
139
|
if (!token || typeof token !== 'string') {
|