@yemi33/minions 0.1.1660 → 0.1.1661
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/CHANGELOG.md +5 -0
- package/engine/cli.js +22 -3
- package/engine/copilot-models.json +1 -1
- package/engine/llm.js +8 -8
- package/engine/runtimes/claude.js +2 -0
- package/engine/runtimes/copilot.js +4 -1
- package/engine/shared.js +45 -12
- package/engine/spawn-agent.js +1 -1
- package/engine/timeout.js +38 -3
- package/engine.js +16 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/cli.js
CHANGED
|
@@ -21,6 +21,17 @@ function engine() {
|
|
|
21
21
|
let _dispatchModule = null;
|
|
22
22
|
function dispatchModule() { if (!_dispatchModule) _dispatchModule = require('./dispatch'); return _dispatchModule; }
|
|
23
23
|
|
|
24
|
+
function normalizeSessionBranch(branch) {
|
|
25
|
+
if (!branch) return null;
|
|
26
|
+
return String(branch).replace(/^refs\/heads\//, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function dispatchSessionBranch(item) {
|
|
30
|
+
return normalizeSessionBranch(
|
|
31
|
+
item?.meta?.branch || item?.branch || item?.meta?.item?.branch || item?.meta?.item?.featureBranch
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
function isEngineProcessAlive(control) {
|
|
25
36
|
if (!control?.pid) return false;
|
|
26
37
|
if (control.pid === process.pid) return true;
|
|
@@ -396,13 +407,21 @@ const commands = {
|
|
|
396
407
|
}
|
|
397
408
|
|
|
398
409
|
if (agentPid) {
|
|
399
|
-
// Load sessionId from session.json for steering support
|
|
410
|
+
// Load sessionId from session.json for steering support, but only
|
|
411
|
+
// when its saved branch still matches the active dispatch branch.
|
|
400
412
|
let sessionId = null;
|
|
401
413
|
try {
|
|
402
414
|
const sj = safeJson(path.join(AGENTS_DIR, agentId, 'session.json'));
|
|
403
|
-
|
|
415
|
+
const expectedBranch = dispatchSessionBranch(item);
|
|
416
|
+
const savedBranch = normalizeSessionBranch(sj?.branch);
|
|
417
|
+
if (sj?.sessionId && (!expectedBranch || savedBranch === expectedBranch)) {
|
|
418
|
+
sessionId = sj.sessionId;
|
|
419
|
+
} else if (sj?.sessionId && expectedBranch) {
|
|
420
|
+
shared.log('warn', `Reattach: ignoring session for ${agentId} on branch ${savedBranch || 'unknown'}; expected ${expectedBranch}`);
|
|
421
|
+
}
|
|
404
422
|
} catch {}
|
|
405
|
-
|
|
423
|
+
const runtimeName = item.runtimeName || item.runtime || item.meta?.runtimeName || item.meta?.runtime || null;
|
|
424
|
+
e.activeProcesses.set(item.id, { proc: { pid: agentPid > 0 ? agentPid : null }, agentId, startedAt: item.created_at, reattached: true, sessionId, runtimeName });
|
|
406
425
|
// Sync work item status to dispatched — atomic write to avoid lifecycle lazy init issues
|
|
407
426
|
if (item.meta?.item?.id && item.meta?.project?.localPath) {
|
|
408
427
|
try {
|
package/engine/llm.js
CHANGED
|
@@ -163,7 +163,14 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
163
163
|
maxBudget, bare, fallbackModel,
|
|
164
164
|
stream, disableBuiltinMcps, suppressAgentsMd, reasoningSummaries,
|
|
165
165
|
};
|
|
166
|
-
|
|
166
|
+
// Capability-gate per-flag opts before prompt construction so adapters can
|
|
167
|
+
// make resume-aware prompt decisions from the same opts used for argv.
|
|
168
|
+
if (!caps.effortLevels) adapterOpts.effort = undefined;
|
|
169
|
+
if (!caps.sessionResume) adapterOpts.sessionId = undefined;
|
|
170
|
+
if (!caps.budgetCap) adapterOpts.maxBudget = undefined;
|
|
171
|
+
if (!caps.bareMode) adapterOpts.bare = undefined;
|
|
172
|
+
if (!caps.fallbackModel) adapterOpts.fallbackModel = undefined;
|
|
173
|
+
const finalPrompt = runtime.buildPrompt(promptText, sysPromptText, adapterOpts);
|
|
167
174
|
|
|
168
175
|
// ── Direct path ──
|
|
169
176
|
const resolved = direct ? _resolveBin(runtime) : null;
|
|
@@ -178,13 +185,6 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
178
185
|
cleanupFiles.push(sysTmpPath);
|
|
179
186
|
adapterOpts.sysPromptFile = sysTmpPath;
|
|
180
187
|
}
|
|
181
|
-
// Capability-gate per-flag opts so the Claude path keeps emitting its
|
|
182
|
-
// historical flag set while Copilot only sees what it understands.
|
|
183
|
-
if (!caps.effortLevels) adapterOpts.effort = undefined;
|
|
184
|
-
if (!caps.sessionResume) adapterOpts.sessionId = undefined;
|
|
185
|
-
if (!caps.budgetCap) adapterOpts.maxBudget = undefined;
|
|
186
|
-
if (!caps.bareMode) adapterOpts.bare = undefined;
|
|
187
|
-
if (!caps.fallbackModel) adapterOpts.fallbackModel = undefined;
|
|
188
188
|
// promptViaArg=true: the adapter splices `--prompt <text>` into args itself.
|
|
189
189
|
if (caps.promptViaArg) adapterOpts.prompt = finalPrompt;
|
|
190
190
|
|
|
@@ -581,6 +581,8 @@ const capabilities = {
|
|
|
581
581
|
streaming: true,
|
|
582
582
|
// `--resume <session-id>` resumes a previous turn
|
|
583
583
|
sessionResume: true,
|
|
584
|
+
// Emits a resumable session ID before the terminal result event
|
|
585
|
+
midRunSessionId: true,
|
|
584
586
|
// Accepts the system prompt via `--system-prompt-file`
|
|
585
587
|
systemPromptFile: true,
|
|
586
588
|
// Honours `--effort low|medium|high|xhigh`
|
|
@@ -344,8 +344,9 @@ function classifyFailure({ code, stdout = '', stderr = '', fallback } = {}) {
|
|
|
344
344
|
// convention from Anthropic tool-use docs and is recognized as "system role"
|
|
345
345
|
// content by every model in the Copilot catalog.
|
|
346
346
|
|
|
347
|
-
function buildPrompt(promptText, sysPromptText) {
|
|
347
|
+
function buildPrompt(promptText, sysPromptText, opts = {}) {
|
|
348
348
|
const user = promptText == null ? '' : String(promptText);
|
|
349
|
+
if (opts && opts.sessionId) return user;
|
|
349
350
|
if (sysPromptText == null || sysPromptText === '') return user;
|
|
350
351
|
return `<system>\n${String(sysPromptText)}\n</system>\n\n${user}`;
|
|
351
352
|
}
|
|
@@ -722,6 +723,8 @@ const capabilities = {
|
|
|
722
723
|
streaming: true,
|
|
723
724
|
// --resume=<id> resumes a session
|
|
724
725
|
sessionResume: true,
|
|
726
|
+
// Copilot only exposes sessionId on the terminal result event
|
|
727
|
+
midRunSessionId: false,
|
|
725
728
|
// No --system-prompt-file flag — system prompt is merged into stdin
|
|
726
729
|
systemPromptFile: false,
|
|
727
730
|
// --effort low|medium|high|xhigh (no 'max' — adapter maps it)
|
package/engine/shared.js
CHANGED
|
@@ -2094,27 +2094,60 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
2094
2094
|
|
|
2095
2095
|
// ─── Cross-Platform Process Kill Helpers ─────────────────────────────────────
|
|
2096
2096
|
|
|
2097
|
+
function normalizeKillPid(proc) {
|
|
2098
|
+
const pid = Number(proc?.pid);
|
|
2099
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function unixChildPids(pid) {
|
|
2103
|
+
if (!Number.isInteger(pid) || pid <= 0) return [];
|
|
2104
|
+
try {
|
|
2105
|
+
return _execSync(`pgrep -P ${pid}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 })
|
|
2106
|
+
.split(/\r?\n/)
|
|
2107
|
+
.map(line => Number(line.trim()))
|
|
2108
|
+
.filter(childPid => Number.isInteger(childPid) && childPid > 0 && childPid !== pid);
|
|
2109
|
+
} catch {
|
|
2110
|
+
return [];
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function killUnixProcessTree(pid, signal, seen = new Set()) {
|
|
2115
|
+
if (!Number.isInteger(pid) || pid <= 0 || seen.has(pid)) return;
|
|
2116
|
+
seen.add(pid);
|
|
2117
|
+
for (const childPid of unixChildPids(pid)) {
|
|
2118
|
+
killUnixProcessTree(childPid, signal, seen);
|
|
2119
|
+
}
|
|
2120
|
+
try { process.kill(pid, signal); } catch { /* process may be dead */ }
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function unrefTimer(timer) {
|
|
2124
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
2125
|
+
return timer;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2097
2128
|
function killGracefully(proc, graceMs = 5000) {
|
|
2098
|
-
|
|
2129
|
+
const pid = normalizeKillPid(proc);
|
|
2130
|
+
if (!pid) return;
|
|
2099
2131
|
if (process.platform === 'win32') {
|
|
2100
|
-
try { _execSync(`taskkill /PID ${
|
|
2101
|
-
setTimeout(() => {
|
|
2102
|
-
try { _execSync(`taskkill /PID ${
|
|
2103
|
-
}, graceMs);
|
|
2132
|
+
try { _execSync(`taskkill /PID ${pid} /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
|
|
2133
|
+
unrefTimer(setTimeout(() => {
|
|
2134
|
+
try { _execSync(`taskkill /PID ${pid} /F /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
|
|
2135
|
+
}, graceMs));
|
|
2104
2136
|
} else {
|
|
2105
|
-
|
|
2106
|
-
setTimeout(() => {
|
|
2107
|
-
|
|
2108
|
-
}, graceMs);
|
|
2137
|
+
killUnixProcessTree(pid, 'SIGTERM');
|
|
2138
|
+
unrefTimer(setTimeout(() => {
|
|
2139
|
+
killUnixProcessTree(pid, 'SIGKILL');
|
|
2140
|
+
}, graceMs));
|
|
2109
2141
|
}
|
|
2110
2142
|
}
|
|
2111
2143
|
|
|
2112
2144
|
function killImmediate(proc) {
|
|
2113
|
-
|
|
2145
|
+
const pid = normalizeKillPid(proc);
|
|
2146
|
+
if (!pid) return;
|
|
2114
2147
|
if (process.platform === 'win32') {
|
|
2115
|
-
try { _execSync(`taskkill /PID ${
|
|
2148
|
+
try { _execSync(`taskkill /PID ${pid} /F /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
|
|
2116
2149
|
} else {
|
|
2117
|
-
|
|
2150
|
+
killUnixProcessTree(pid, 'SIGKILL');
|
|
2118
2151
|
}
|
|
2119
2152
|
}
|
|
2120
2153
|
|
package/engine/spawn-agent.js
CHANGED
|
@@ -100,9 +100,9 @@ function parseSpawnArgs(argv) {
|
|
|
100
100
|
* }
|
|
101
101
|
*/
|
|
102
102
|
function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, opts, passthrough, addDirs }) {
|
|
103
|
-
const finalPrompt = runtime.buildPrompt(promptText, sysPromptText);
|
|
104
103
|
const adapterOpts = { ...opts };
|
|
105
104
|
if (Array.isArray(addDirs) && addDirs.length) adapterOpts.addDirs = addDirs;
|
|
105
|
+
const finalPrompt = runtime.buildPrompt(promptText, sysPromptText, adapterOpts);
|
|
106
106
|
const deliveryMode = typeof runtime.getPromptDeliveryMode === 'function'
|
|
107
107
|
? runtime.getPromptDeliveryMode(adapterOpts)
|
|
108
108
|
: (runtime.capabilities && runtime.capabilities.promptViaArg ? 'arg' : 'stdin');
|
package/engine/timeout.js
CHANGED
|
@@ -57,6 +57,27 @@ function checkIdleThreshold(config) {
|
|
|
57
57
|
// How long to wait for a steered agent to exit before retrying the kill
|
|
58
58
|
const STEERING_KILL_RETRY_MS = 30000;
|
|
59
59
|
|
|
60
|
+
function runtimeSupportsMidRunSessionId(info) {
|
|
61
|
+
if (typeof info?.midRunSessionId === 'boolean') return info.midRunSessionId;
|
|
62
|
+
if (typeof info?.runtime?.capabilities?.midRunSessionId === 'boolean') return info.runtime.capabilities.midRunSessionId;
|
|
63
|
+
if (info?.runtimeName) {
|
|
64
|
+
try {
|
|
65
|
+
const { resolveRuntime } = require('./runtimes');
|
|
66
|
+
const runtime = resolveRuntime(info.runtimeName);
|
|
67
|
+
if (typeof runtime.capabilities?.midRunSessionId === 'boolean') return runtime.capabilities.midRunSessionId;
|
|
68
|
+
} catch {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function rememberDeferredSteering(info, steerEntry) {
|
|
76
|
+
const existing = new Set(Array.isArray(info._deferredSteeringFiles) ? info._deferredSteeringFiles : []);
|
|
77
|
+
if (steerEntry?.path) existing.add(steerEntry.path);
|
|
78
|
+
info._deferredSteeringFiles = Array.from(existing);
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
function checkSteering(config) {
|
|
61
82
|
const activeProcesses = engine().activeProcesses;
|
|
62
83
|
for (const [id, info] of activeProcesses) {
|
|
@@ -79,7 +100,10 @@ function checkSteering(config) {
|
|
|
79
100
|
// Skip if already being steered (prevents double-kill race)
|
|
80
101
|
if (info._steeringMessage || info._steeringAt) continue;
|
|
81
102
|
|
|
82
|
-
const alreadyPending = new Set(
|
|
103
|
+
const alreadyPending = new Set([
|
|
104
|
+
...(info._pendingSteeringFiles || []).map(entry => entry.path || entry),
|
|
105
|
+
...(info._deferredSteeringFiles || []),
|
|
106
|
+
]);
|
|
83
107
|
const unread = steering.listUnreadSteeringMessages(info.agentId);
|
|
84
108
|
for (const empty of unread.filter(entry => !entry.message.trim())) {
|
|
85
109
|
shared.safeUnlink(empty.path);
|
|
@@ -90,8 +114,19 @@ function checkSteering(config) {
|
|
|
90
114
|
|
|
91
115
|
const sessionId = info.sessionId;
|
|
92
116
|
if (!sessionId) {
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
if (!runtimeSupportsMidRunSessionId(info)) {
|
|
118
|
+
log('info', `Steering: no mid-run sessionId for ${info.agentId} (${id}) — queued until resumable checkpoint`);
|
|
119
|
+
rememberDeferredSteering(info, steerEntry);
|
|
120
|
+
try {
|
|
121
|
+
const liveLogPath = path.join(AGENTS_DIR, info.agentId, 'live-output.log');
|
|
122
|
+
fs.appendFileSync(liveLogPath, `\n[steering] Message received. This runtime has not emitted a resumable session yet, so the message is queued until the agent reaches a resumable checkpoint or the next dispatch.\n`);
|
|
123
|
+
} catch { /* optional */ }
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// No session to resume for a runtime that should have emitted one — kill
|
|
128
|
+
// agent and leave message unread in inbox for retry. Previously this
|
|
129
|
+
// silently skipped for up to 5m then deleted the message (#627).
|
|
95
130
|
log('info', `Steering: no sessionId for ${info.agentId} (${id}) — killing and keeping unread message in inbox`);
|
|
96
131
|
|
|
97
132
|
// Append to live output so user sees confirmation in the dashboard
|
package/engine.js
CHANGED
|
@@ -1043,6 +1043,16 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1043
1043
|
fs.appendFileSync(liveOutputPath, `[${new Date().toISOString()}] pid: ${proc.pid ?? 'unknown'}\n`);
|
|
1044
1044
|
} catch { /* log stamp is best-effort — don't block spawn on fs failure */ }
|
|
1045
1045
|
|
|
1046
|
+
const initialProcInfo = {
|
|
1047
|
+
proc,
|
|
1048
|
+
agentId,
|
|
1049
|
+
startedAt,
|
|
1050
|
+
runtimeName,
|
|
1051
|
+
sessionId: cachedSessionId,
|
|
1052
|
+
_pendingSteeringFiles: pendingSteering.entries,
|
|
1053
|
+
};
|
|
1054
|
+
activeProcesses.set(id, initialProcInfo);
|
|
1055
|
+
|
|
1046
1056
|
const MAX_OUTPUT = 1024 * 1024; // 1MB
|
|
1047
1057
|
let stdout = '';
|
|
1048
1058
|
let stderr = '';
|
|
@@ -1204,6 +1214,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1204
1214
|
proc: resumeProc,
|
|
1205
1215
|
agentId,
|
|
1206
1216
|
startedAt: procInfo.startedAt,
|
|
1217
|
+
runtimeName,
|
|
1207
1218
|
sessionId: steerSessionId,
|
|
1208
1219
|
lastRealOutputAt: Date.now(),
|
|
1209
1220
|
_pendingSteeringFiles: mergePendingSteeringEntries(
|
|
@@ -1446,12 +1457,15 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1446
1457
|
// realActivityMap was already seeded immediately after runFile() returned (#W-mo25loq8kjer);
|
|
1447
1458
|
// don't re-seed here — the stdout/stderr handlers above can already have updated it with
|
|
1448
1459
|
// a fresher timestamp, and overwriting would clobber the real "last activity" signal.
|
|
1460
|
+
const existingProcInfo = activeProcesses.get(id) || {};
|
|
1449
1461
|
activeProcesses.set(id, {
|
|
1462
|
+
...existingProcInfo,
|
|
1450
1463
|
proc,
|
|
1451
1464
|
agentId,
|
|
1452
1465
|
startedAt,
|
|
1453
|
-
|
|
1454
|
-
|
|
1466
|
+
runtimeName,
|
|
1467
|
+
sessionId: existingProcInfo.sessionId || cachedSessionId,
|
|
1468
|
+
_pendingSteeringFiles: mergePendingSteeringEntries(existingProcInfo._pendingSteeringFiles, pendingSteering.entries),
|
|
1455
1469
|
});
|
|
1456
1470
|
|
|
1457
1471
|
updateAgentStatus(id, AGENT_STATUS.RUNNING, `Process spawned for ${agentId}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1661",
|
|
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"
|