@yemi33/minions 0.1.1927 → 0.1.1929
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/cc-worker-pool.js +30 -9
- package/engine/copilot-models.json +1 -1
- package/engine/features.js +34 -9
- package/engine.js +15 -0
- package/package.json +1 -1
package/engine/cc-worker-pool.js
CHANGED
|
@@ -98,6 +98,19 @@ function _hashMcpServers(mcpServers) {
|
|
|
98
98
|
return crypto.createHash('sha256').update(json).digest('hex');
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Bug B (issue #2479): `model` and `effort` were stored on the Worker but
|
|
102
|
+
// never forwarded to ACP `session/new`, so any `ccModel` override was silently
|
|
103
|
+
// dropped on the pool path. Build the params object here and only include
|
|
104
|
+
// fields that are defined — sending `model: undefined` would force the daemon
|
|
105
|
+
// onto its built-in default instead of letting it pick whatever the user has
|
|
106
|
+
// configured globally.
|
|
107
|
+
function _buildSessionNewParams({ cwd, mcpServers, model, effort }) {
|
|
108
|
+
const params = { cwd, mcpServers: mcpServers || [] };
|
|
109
|
+
if (model !== undefined && model !== null && model !== '') params.model = model;
|
|
110
|
+
if (effort !== undefined && effort !== null && effort !== '') params.effort = effort;
|
|
111
|
+
return params;
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
class Worker {
|
|
102
115
|
constructor({ tabId, model, effort, mcpServers, mcpServersHash, systemPromptHash, cwd }) {
|
|
103
116
|
this.tabId = tabId;
|
|
@@ -170,7 +183,9 @@ class Worker {
|
|
|
170
183
|
earlyExitPromise,
|
|
171
184
|
]);
|
|
172
185
|
const result = await Promise.race([
|
|
173
|
-
this._call('session/new', {
|
|
186
|
+
this._call('session/new', _buildSessionNewParams({
|
|
187
|
+
cwd: this.cwd, mcpServers: this.mcpServers, model: this.model, effort: this.effort,
|
|
188
|
+
})),
|
|
174
189
|
earlyExitPromise,
|
|
175
190
|
]);
|
|
176
191
|
this.sessionId = result && result.sessionId;
|
|
@@ -344,7 +359,7 @@ class Worker {
|
|
|
344
359
|
this._notify('session/cancel', { sessionId: this.inflight.sessionId });
|
|
345
360
|
}
|
|
346
361
|
|
|
347
|
-
async newSession({ mcpServers, systemPromptHash }) {
|
|
362
|
+
async newSession({ mcpServers, systemPromptHash, model, effort }) {
|
|
348
363
|
// Cancel any inflight before swapping the underlying session.
|
|
349
364
|
if (this.inflight) {
|
|
350
365
|
this.cancel();
|
|
@@ -360,10 +375,15 @@ class Worker {
|
|
|
360
375
|
wait();
|
|
361
376
|
});
|
|
362
377
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
378
|
+
// Bug B (issue #2479): if the caller is rotating the session because the
|
|
379
|
+
// system prompt changed, they may also be passing a fresh model/effort —
|
|
380
|
+
// update bookkeeping BEFORE session/new so the new fields land on the
|
|
381
|
+
// daemon. Falls through to whatever we already had when callers omit them.
|
|
382
|
+
if (model !== undefined) this.model = model;
|
|
383
|
+
if (effort !== undefined) this.effort = effort;
|
|
384
|
+
const result = await this._call('session/new', _buildSessionNewParams({
|
|
385
|
+
cwd: this.cwd, mcpServers: mcpServers || [], model: this.model, effort: this.effort,
|
|
386
|
+
}));
|
|
367
387
|
this.sessionId = result && result.sessionId;
|
|
368
388
|
this.systemPromptHash = systemPromptHash;
|
|
369
389
|
this.firstSystemPromptSent = false;
|
|
@@ -425,9 +445,9 @@ async function getSession({ tabId, model, effort, mcpServers, systemPromptHash,
|
|
|
425
445
|
} else if (worker.systemPromptHash !== systemPromptHash) {
|
|
426
446
|
// System prompt changed → keep the warm process, drop the session
|
|
427
447
|
// and create a fresh one. Saves the ~2.1 s initialize handshake.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
worker.
|
|
448
|
+
// Pass model/effort so the NEW session picks up any override the
|
|
449
|
+
// caller has rotated in (Bug B / issue #2479).
|
|
450
|
+
await worker.newSession({ mcpServers, systemPromptHash, model, effort });
|
|
431
451
|
worker.lastUsedAt = _internals.now();
|
|
432
452
|
} else {
|
|
433
453
|
// Warm reuse — only update bookkeeping. model/effort changes on a
|
|
@@ -507,6 +527,7 @@ module.exports = {
|
|
|
507
527
|
_internals,
|
|
508
528
|
_tabs,
|
|
509
529
|
_reapIdleTabs,
|
|
530
|
+
_buildSessionNewParams,
|
|
510
531
|
IDLE_REAPER_MS,
|
|
511
532
|
REAPER_INTERVAL_MS,
|
|
512
533
|
};
|
package/engine/features.js
CHANGED
|
@@ -2,27 +2,51 @@
|
|
|
2
2
|
* engine/features.js — Feature flag registry. Recipe in CLAUDE.md → "Feature Flags".
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
// Entry shape: id → { description, default: bool, addedIn?: version, expires?: ISO-date }
|
|
5
|
+
// Entry shape: id → { description, default: bool, addedIn?: version, expires?: ISO-date, requiredCcRuntime?: string }
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// Optional fields:
|
|
8
|
+
// `requiredCcRuntime` (e.g. 'copilot') signals that the flag only takes
|
|
9
|
+
// effect when `resolveCcCli(engine)` matches. The dashboard uses this to
|
|
10
|
+
// render the toggle as disabled with a tooltip when the configured CC
|
|
11
|
+
// runtime is incompatible. Engine code still has to gate explicitly via
|
|
12
|
+
// `isFeatureOn` + a runtime check — `requiredCcRuntime` is a UX hint only.
|
|
13
|
+
//
|
|
14
|
+
// REGISTRY GROWTH POLICY — DO NOT DELETE THE FRAMEWORK.
|
|
8
15
|
//
|
|
9
|
-
// The `FEATURES = {}` literal below is intentional scaffolding, not dead code.
|
|
10
16
|
// The surrounding framework (isFeatureOn, listFeatures, hasFeature, env-var
|
|
11
17
|
// override resolver, expiration timestamps) is load-bearing: dashboard.js
|
|
12
18
|
// boot wires it into /api/features (list), /api/features/toggle, and the
|
|
13
19
|
// `window.MINIONS_FEATURES` client bootstrap. Deleting the registry — even
|
|
14
|
-
//
|
|
20
|
+
// if it became empty — would break dashboard startup and the experimental-flags UI.
|
|
15
21
|
//
|
|
16
22
|
// New flags belong here. Register them in this object instead of removing
|
|
17
|
-
//
|
|
18
|
-
// until then the empty object is the correct, expected shape.
|
|
23
|
+
// entries. Keep at least one entry so the framework remains demonstrably wired.
|
|
19
24
|
//
|
|
20
25
|
// Decision logged in the 2026-05-13 daily architecture & bug review meeting
|
|
21
26
|
// (knowledge/architecture/2026-05-13-ripley-meeting-conclusion-daily-architecture-bug-review-2.md,
|
|
22
|
-
// PR-C, Option B — keep framework, document the
|
|
27
|
+
// PR-C, Option B — keep framework, document the registry).
|
|
23
28
|
const FEATURES = {
|
|
24
|
-
//
|
|
25
|
-
//
|
|
29
|
+
// ccUseWorkerPool — sub-tasks B/C/D of W-mp2w003600196c51 (CC perf).
|
|
30
|
+
// Routes Command Center / doc-chat through engine/cc-worker-pool.js
|
|
31
|
+
// (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI
|
|
32
|
+
// per turn. Saves ~14 s of cold-start cost. Copilot-only — the pool drives
|
|
33
|
+
// ACP which Claude does not implement.
|
|
34
|
+
//
|
|
35
|
+
// Resolution: `shared.resolveCcUseWorkerPool(engine)` (engine/shared.js)
|
|
36
|
+
// is the canonical predicate every dashboard.js call site uses. Explicit
|
|
37
|
+
// `engine.ccUseWorkerPool` true/false in config wins; otherwise the helper
|
|
38
|
+
// defaults ON for `copilot` and OFF for `claude`. PR #2492 flipped the
|
|
39
|
+
// default ON for copilot; see that PR for the cold-spawn measurements.
|
|
40
|
+
//
|
|
41
|
+
// `requiredCcRuntime: 'copilot'` here is a UX hint — the Settings panel
|
|
42
|
+
// greys out the toggle when the resolved CC runtime mismatches so users
|
|
43
|
+
// see the constraint instead of toggling a no-op.
|
|
44
|
+
'ccUseWorkerPool': {
|
|
45
|
+
description: 'Route Command Center / doc-chat through a persistent `copilot --acp` worker per tab instead of spawning a fresh CLI per turn (~14s cold-start savings). Copilot-only — has no effect when the CC runtime is Claude.',
|
|
46
|
+
default: false,
|
|
47
|
+
addedIn: '0.1.1916',
|
|
48
|
+
requiredCcRuntime: 'copilot',
|
|
49
|
+
},
|
|
26
50
|
};
|
|
27
51
|
|
|
28
52
|
const ENV_TRUTHY = new Set(['1', 'true', 'on', 'yes']);
|
|
@@ -64,6 +88,7 @@ function listFeatures(config, registry = FEATURES) {
|
|
|
64
88
|
addedIn: meta.addedIn || null,
|
|
65
89
|
expires: meta.expires || null,
|
|
66
90
|
expired: expiresAt < now,
|
|
91
|
+
requiredCcRuntime: meta.requiredCcRuntime || null,
|
|
67
92
|
};
|
|
68
93
|
});
|
|
69
94
|
}
|
package/engine.js
CHANGED
|
@@ -823,7 +823,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
823
823
|
shared.assertWorktreeOutsideProject(worktreePath, rootDir);
|
|
824
824
|
|
|
825
825
|
// If branch is already checked out in an existing worktree, reuse it
|
|
826
|
+
_phaseT.findExistingStart = Date.now();
|
|
826
827
|
const existingWt = await findExistingWorktree(rootDir, branchName);
|
|
828
|
+
_phaseT.findExistingEnd = Date.now();
|
|
827
829
|
if (existingWt) {
|
|
828
830
|
// Same guard for reuse — a previously-created bad worktree must not
|
|
829
831
|
// be silently reused either; the cleanup sweep flags these so the
|
|
@@ -834,13 +836,16 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
834
836
|
// Probe origin first — locally-created branches that were never pushed
|
|
835
837
|
// (orphan/timeout retry before first push) would otherwise emit a
|
|
836
838
|
// "couldn't find remote ref" warn pair on every reuse.
|
|
839
|
+
_phaseT.reuseSyncStart = Date.now();
|
|
837
840
|
await syncReusedWorktree(rootDir, existingWt, branchName, _gitOpts);
|
|
841
|
+
_phaseT.reuseSyncEnd = Date.now();
|
|
838
842
|
} else if (READ_ONLY_ROOT_TASK_TYPES.has(type) && !isPipelineBranchName(branchName)) {
|
|
839
843
|
// Read-only tasks — no worktree needed, run in rootDir
|
|
840
844
|
log('info', `${type}: read-only task, no worktree needed — running in rootDir`);
|
|
841
845
|
branchName = null;
|
|
842
846
|
worktreePath = null;
|
|
843
847
|
} else {
|
|
848
|
+
_phaseT.createWorktreeStart = Date.now();
|
|
844
849
|
try {
|
|
845
850
|
if (!fs.existsSync(worktreePath)) {
|
|
846
851
|
const isSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
|
|
@@ -965,6 +970,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
965
970
|
return null;
|
|
966
971
|
}
|
|
967
972
|
}
|
|
973
|
+
_phaseT.createWorktreeEnd = Date.now();
|
|
968
974
|
}
|
|
969
975
|
|
|
970
976
|
// Shared-branch preflight (#2439): refuse to dispatch into a dirty shared
|
|
@@ -974,7 +980,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
974
980
|
// fires AFTER spawn — converting orchestration hygiene into fake
|
|
975
981
|
// implementation failures and cascading dependent items.
|
|
976
982
|
if (worktreePath && fs.existsSync(worktreePath) && meta?.branchStrategy === 'shared-branch' && branchName) {
|
|
983
|
+
_phaseT.cleanCheckStart = Date.now();
|
|
977
984
|
const cleanResult = await assertCleanSharedWorktree(rootDir, worktreePath, branchName, id, _gitOpts);
|
|
985
|
+
_phaseT.cleanCheckEnd = Date.now();
|
|
978
986
|
if (!cleanResult.clean) {
|
|
979
987
|
const previewFiles = (cleanResult.dirtyFiles || []).slice(0, 5).join(', ');
|
|
980
988
|
const reasonMsg = `DIRTY_WORKTREE: shared branch ${branchName} worktree at ${worktreePath} is dirty (${cleanResult.reason}); ${cleanResult.dirtyFiles?.length || 0} file(s)${previewFiles ? ': ' + previewFiles : ''}`;
|
|
@@ -1281,6 +1289,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1281
1289
|
// Inject dirty file list when worktree has uncommitted changes (e.g., max_turns retry)
|
|
1282
1290
|
// This signals to the respawned agent that prior work exists in the worktree (#960)
|
|
1283
1291
|
if (worktreePath && fs.existsSync(worktreePath)) {
|
|
1292
|
+
_phaseT.dirtyProbeStart = Date.now();
|
|
1284
1293
|
try {
|
|
1285
1294
|
const dirtyResult = await execAsync('git status --porcelain', { ..._gitOpts, cwd: worktreePath, timeout: 10000 });
|
|
1286
1295
|
const dirtyOutput = (dirtyResult.stdout || '').trim();
|
|
@@ -1298,6 +1307,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1298
1307
|
log('info', `Injected ${dirtyFiles.length} dirty files into prompt for ${id}`);
|
|
1299
1308
|
}
|
|
1300
1309
|
} catch (e) { log('warn', `git status --porcelain for dirty files: ${e.message}`); }
|
|
1310
|
+
_phaseT.dirtyProbeEnd = Date.now();
|
|
1301
1311
|
}
|
|
1302
1312
|
|
|
1303
1313
|
// Safety check: warn if a write-capable task is running in the main repo without a worktree
|
|
@@ -1504,6 +1514,11 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1504
1514
|
const timings = {
|
|
1505
1515
|
prompt: _diff('start', 'afterPrompt'),
|
|
1506
1516
|
worktree_total: _diff('afterPrompt', 'afterWorktree'),
|
|
1517
|
+
wt_find_existing: _diff('findExistingStart', 'findExistingEnd'),
|
|
1518
|
+
wt_reuse_sync: _diff('reuseSyncStart', 'reuseSyncEnd'),
|
|
1519
|
+
wt_create: _diff('createWorktreeStart', 'createWorktreeEnd'),
|
|
1520
|
+
wt_clean_check: _diff('cleanCheckStart', 'cleanCheckEnd'),
|
|
1521
|
+
wt_dirty_probe: _diff('dirtyProbeStart', 'dirtyProbeEnd'),
|
|
1507
1522
|
dep_fetch: _diff('depFetchStart', 'depFetchEnd'),
|
|
1508
1523
|
dep_preflight: _diff('depPreflightStart', 'depPreflightEnd'),
|
|
1509
1524
|
dep_merge: _diff('depMergeStart', 'depMergeEnd'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1929",
|
|
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"
|