@yemi33/minions 0.1.492 → 0.1.494
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 +3 -1
- package/engine/cli.js +31 -5
- package/engine/github.js +77 -1
- package/engine.js +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.494 (2026-04-07)
|
|
4
4
|
|
|
5
5
|
### Fixes
|
|
6
|
+
- GitHub PR poll backoff for inaccessible repos (closes #377) (#386)
|
|
7
|
+
- reconcile agent completions during engine downtime (closes #376) (#385)
|
|
6
8
|
- settings button shows modal immediately with loading state
|
|
7
9
|
- CC respects user scroll position — no auto-scroll when reading history
|
|
8
10
|
- steering feedback — toast notification, ensure polling resumes
|
package/engine/cli.js
CHANGED
|
@@ -221,17 +221,43 @@ const commands = {
|
|
|
221
221
|
const hasError = output.includes('"is_error":true') || output.includes('"is_error": true');
|
|
222
222
|
if (!hasResult && !hasError) continue;
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
const result = isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR;
|
|
226
|
-
|
|
227
|
-
e.log('info', `Orphan recovery: ${agentId} (${item.id}) completed while engine was down — result: ${result}`);
|
|
224
|
+
let isSuccess = hasResult && !hasError;
|
|
228
225
|
|
|
229
|
-
// Extract PRs from output
|
|
226
|
+
// Extract PRs from output first — if PRs were created, the agent succeeded
|
|
227
|
+
// regardless of intermediate error lines in the log
|
|
230
228
|
let prsCreated = 0;
|
|
231
229
|
try {
|
|
232
230
|
prsCreated = lifecycle.syncPrsFromOutput(output, agentId, item.meta, config);
|
|
233
231
|
} catch (err) { e.log('warn', `Orphan PR sync: ${err.message}`); }
|
|
234
232
|
|
|
233
|
+
// If PRs were created or a matching PR exists, treat as success
|
|
234
|
+
if (!isSuccess && prsCreated > 0) {
|
|
235
|
+
e.log('info', `Orphan recovery: ${agentId} (${item.id}) has ${prsCreated} PR(s) — overriding to success`);
|
|
236
|
+
isSuccess = true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Fallback: check pull-requests.json for a matching PR by work item ID
|
|
240
|
+
if (!isSuccess && item.meta?.item?.id) {
|
|
241
|
+
try {
|
|
242
|
+
const projName = item.meta.project?.name;
|
|
243
|
+
if (projName) {
|
|
244
|
+
const prPath = path.join(MINIONS_DIR, 'projects', projName, 'pull-requests.json');
|
|
245
|
+
const prs = safeJson(prPath) || [];
|
|
246
|
+
const matchingPr = prs.find(pr =>
|
|
247
|
+
(pr.prdItems || []).includes(item.meta.item.id) &&
|
|
248
|
+
pr.status !== 'abandoned' && pr.status !== 'closed'
|
|
249
|
+
);
|
|
250
|
+
if (matchingPr) {
|
|
251
|
+
e.log('info', `Orphan recovery: ${agentId} (${item.id}) has matching PR ${matchingPr.id} — overriding to success`);
|
|
252
|
+
isSuccess = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch (err) { e.log('warn', `Orphan PR lookup: ${err.message}`); }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const result = isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR;
|
|
259
|
+
e.log('info', `Orphan recovery: ${agentId} (${item.id}) completed while engine was down — result: ${result}`);
|
|
260
|
+
|
|
235
261
|
// Update work item status
|
|
236
262
|
if (item.meta?.item?.id) {
|
|
237
263
|
const status = isSuccess ? WI_STATUS.DONE : WI_STATUS.FAILED;
|
package/engine/github.js
CHANGED
|
@@ -30,6 +30,44 @@ function getRepoSlug(project) {
|
|
|
30
30
|
return `${org}/${repo}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// ─── Per-Repo Poll Backoff ──────────────────────────────────────────────────
|
|
34
|
+
// Tracks consecutive poll failures per repo slug to avoid spamming logs when
|
|
35
|
+
// a repo is inaccessible. Backoff doubles each failure: 2min, 4min, 8min, 16min, max 30min.
|
|
36
|
+
const _ghPollBackoff = new Map(); // slug → { failures, backoffUntil }
|
|
37
|
+
const GH_POLL_BACKOFF_BASE_MS = 2 * 60 * 1000; // 2 minutes (one poll cycle)
|
|
38
|
+
const GH_POLL_BACKOFF_MAX_MS = 30 * 60 * 1000; // 30 minutes cap
|
|
39
|
+
|
|
40
|
+
/** Check if a repo slug is currently in backoff. Returns true if should skip. */
|
|
41
|
+
function isSlugInBackoff(slug) {
|
|
42
|
+
const entry = _ghPollBackoff.get(slug);
|
|
43
|
+
if (!entry) return false;
|
|
44
|
+
return Date.now() < entry.backoffUntil;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Record a poll failure for a repo slug, applying exponential backoff. */
|
|
48
|
+
function recordSlugFailure(slug) {
|
|
49
|
+
const existing = _ghPollBackoff.get(slug);
|
|
50
|
+
const failures = (existing?.failures || 0) + 1;
|
|
51
|
+
const backoffMs = Math.min(GH_POLL_BACKOFF_BASE_MS * Math.pow(2, failures - 1), GH_POLL_BACKOFF_MAX_MS);
|
|
52
|
+
_ghPollBackoff.set(slug, { failures, backoffUntil: Date.now() + backoffMs });
|
|
53
|
+
if (failures === 1) {
|
|
54
|
+
log('warn', `GitHub poll: repo ${slug} failed — will retry in ${Math.round(backoffMs / 1000)}s`);
|
|
55
|
+
} else {
|
|
56
|
+
log('warn', `GitHub poll: repo ${slug} failed ${failures} times — backoff ${Math.round(backoffMs / 1000)}s`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Reset backoff for a repo slug after a successful poll. */
|
|
61
|
+
function resetSlugBackoff(slug) {
|
|
62
|
+
if (_ghPollBackoff.has(slug)) {
|
|
63
|
+
const entry = _ghPollBackoff.get(slug);
|
|
64
|
+
if (entry.failures > 0) {
|
|
65
|
+
log('info', `GitHub poll: repo ${slug} recovered after ${entry.failures} failure(s)`);
|
|
66
|
+
}
|
|
67
|
+
_ghPollBackoff.delete(slug);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
33
71
|
/** Run a `gh api` call and parse JSON result. Returns null on failure. */
|
|
34
72
|
function ghApi(endpoint, slug) {
|
|
35
73
|
try {
|
|
@@ -42,6 +80,20 @@ function ghApi(endpoint, slug) {
|
|
|
42
80
|
}
|
|
43
81
|
}
|
|
44
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Run a `gh api` call with per-slug backoff tracking. Returns null on failure.
|
|
85
|
+
* On success, resets the slug's backoff. On failure, increments it.
|
|
86
|
+
*/
|
|
87
|
+
function ghApiWithBackoff(endpoint, slug) {
|
|
88
|
+
const result = ghApi(endpoint, slug);
|
|
89
|
+
if (result === null) {
|
|
90
|
+
recordSlugFailure(slug);
|
|
91
|
+
} else {
|
|
92
|
+
resetSlugBackoff(slug);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
45
97
|
// ─── Shared PR Polling Loop ─────────────────────────────────────────────────
|
|
46
98
|
|
|
47
99
|
async function forEachActiveGhPr(config, callback) {
|
|
@@ -52,10 +104,21 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
52
104
|
const slug = getRepoSlug(project);
|
|
53
105
|
if (!slug) continue;
|
|
54
106
|
|
|
107
|
+
// Skip projects in backoff (inaccessible repo)
|
|
108
|
+
if (isSlugInBackoff(slug)) continue;
|
|
109
|
+
|
|
55
110
|
const prs = getPrs(project);
|
|
56
111
|
const activePrs = prs.filter(pr => pr.status === PR_STATUS.ACTIVE);
|
|
57
112
|
if (activePrs.length === 0) continue;
|
|
58
113
|
|
|
114
|
+
// Probe repo accessibility before iterating PRs — avoids N warnings per inaccessible repo
|
|
115
|
+
const probe = ghApi('', slug);
|
|
116
|
+
if (probe === null) {
|
|
117
|
+
recordSlugFailure(slug);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
resetSlugBackoff(slug);
|
|
121
|
+
|
|
59
122
|
let projectUpdated = 0;
|
|
60
123
|
|
|
61
124
|
for (const pr of activePrs) {
|
|
@@ -101,6 +164,7 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
101
164
|
const ghMatch = pr.url.match(/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
|
|
102
165
|
if (!ghMatch) continue;
|
|
103
166
|
const slug = ghMatch[1];
|
|
167
|
+
if (isSlugInBackoff(slug)) continue;
|
|
104
168
|
const prNum = ghMatch[2];
|
|
105
169
|
try {
|
|
106
170
|
const updated = await callback(null, pr, prNum, slug);
|
|
@@ -370,9 +434,16 @@ async function reconcilePrs(config) {
|
|
|
370
434
|
const slug = getRepoSlug(project);
|
|
371
435
|
if (!slug) continue;
|
|
372
436
|
|
|
437
|
+
// Skip projects in backoff (inaccessible repo)
|
|
438
|
+
if (isSlugInBackoff(slug)) continue;
|
|
439
|
+
|
|
373
440
|
// Fetch open PRs
|
|
374
441
|
const prsData = ghApi('/pulls?state=open&per_page=100', slug);
|
|
375
|
-
if (!prsData || !Array.isArray(prsData))
|
|
442
|
+
if (!prsData || !Array.isArray(prsData)) {
|
|
443
|
+
recordSlugFailure(slug);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
resetSlugBackoff(slug);
|
|
376
447
|
|
|
377
448
|
const ghPrs = prsData.filter(pr => {
|
|
378
449
|
const branch = pr.head?.ref || '';
|
|
@@ -498,5 +569,10 @@ module.exports = {
|
|
|
498
569
|
pollPrHumanComments,
|
|
499
570
|
reconcilePrs,
|
|
500
571
|
checkLiveReviewStatus,
|
|
572
|
+
// Exported for testing
|
|
573
|
+
isSlugInBackoff,
|
|
574
|
+
recordSlugFailure,
|
|
575
|
+
resetSlugBackoff,
|
|
576
|
+
_ghPollBackoff,
|
|
501
577
|
};
|
|
502
578
|
|
package/engine.js
CHANGED
|
@@ -838,7 +838,10 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
|
838
838
|
const prLinks = shared.getPrLinks();
|
|
839
839
|
let reconciled = 0;
|
|
840
840
|
for (const wi of items) {
|
|
841
|
-
|
|
841
|
+
// Reconcile pending items AND failed items that have a matching PR
|
|
842
|
+
// (failed items may have been incorrectly marked during engine downtime)
|
|
843
|
+
if (wi._pr && wi.status !== WI_STATUS.FAILED) continue;
|
|
844
|
+
if (wi.status !== WI_STATUS.PENDING && wi.status !== WI_STATUS.FAILED) continue;
|
|
842
845
|
if (onlyIds && !onlyIds.has(wi.id)) continue;
|
|
843
846
|
|
|
844
847
|
let exactPr = allPrs.find(pr => (pr.prdItems || []).includes(wi.id));
|
|
@@ -849,6 +852,10 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
|
849
852
|
if (exactPr) {
|
|
850
853
|
wi.status = WI_STATUS.DONE;
|
|
851
854
|
wi._pr = exactPr.id;
|
|
855
|
+
// Clear failure artifacts if reconciling a previously failed item
|
|
856
|
+
if (wi.failReason) delete wi.failReason;
|
|
857
|
+
if (wi.failedAt) delete wi.failedAt;
|
|
858
|
+
if (!wi.completedAt) wi.completedAt = new Date().toISOString();
|
|
852
859
|
reconciled++;
|
|
853
860
|
}
|
|
854
861
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.494",
|
|
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"
|