@yemi33/minions 0.1.954 → 0.1.956
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 -3
- package/dashboard/js/refresh.js +2 -0
- package/dashboard/js/render-dispatch.js +29 -1
- package/dashboard/js/settings.js +2 -0
- package/dashboard/pages/engine.html +2 -0
- package/dashboard.js +4 -0
- package/engine/ado.js +48 -2
- package/engine/github.js +26 -2
- package/engine/shared.js +54 -0
- package/engine.js +14 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.956 (2026-04-14)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- wire agentBusyReassignMs into settings UI and persist
|
|
7
|
+
- ADO throttle detection, poll guards, and dashboard banner (#1051)
|
|
6
8
|
- gate auto-fix conflict dispatch behind autoFixConflicts flag
|
|
7
9
|
- add PR/build status CLI shim and agent guidance
|
|
8
10
|
- make ADO poll frequency configurable and ungate reconcilePrs
|
|
@@ -21,8 +23,6 @@
|
|
|
21
23
|
- fix dashboard plan-pause nested lock violation (#968)
|
|
22
24
|
- add missing branch_name to central dispatch vars (#967)
|
|
23
25
|
- fix dispatch.js mutator fallback to use nullish coalescing (#966)
|
|
24
|
-
- fix stall recovery nested lock violation (#965)
|
|
25
|
-
- fix pending-rebases.json race condition with file locking (#964)
|
|
26
26
|
|
|
27
27
|
### Fixes
|
|
28
28
|
- fix ENGINE_DEFAULTS ref and derive boolean settings from defaults
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -75,6 +75,8 @@ function _processStatusUpdate(data) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
if (_changed('version', data.version)) renderVersionBanner(data.version);
|
|
78
|
+
if (_changed('adoThrottle', data.adoThrottle)) renderAdoThrottleAlert(data.adoThrottle);
|
|
79
|
+
if (_changed('ghThrottle', data.ghThrottle)) renderGhThrottleAlert(data.ghThrottle);
|
|
78
80
|
if (_changed('dispatch', data.dispatch)) renderDispatch(data.dispatch);
|
|
79
81
|
window._lastDispatch = data.dispatch;
|
|
80
82
|
window._lastWorkItems = data.workItems || [];
|
|
@@ -72,6 +72,34 @@ function renderEngineAlert(state, staleMs) {
|
|
|
72
72
|
el.style.display = 'flex';
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function renderAdoThrottleAlert(adoThrottle) {
|
|
76
|
+
const el = document.getElementById('ado-throttle-alert');
|
|
77
|
+
if (!el) return;
|
|
78
|
+
if (!adoThrottle || !adoThrottle.throttled) {
|
|
79
|
+
el.style.display = 'none';
|
|
80
|
+
el.innerHTML = '';
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const resumeTime = new Date(adoThrottle.retryAfter).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
84
|
+
el.innerHTML =
|
|
85
|
+
'<span class="engine-alert-msg">⚠️ ADO rate-limited — resume ~' + resumeTime + ' (' + adoThrottle.consecutiveHits + ' consecutive hit' + (adoThrottle.consecutiveHits !== 1 ? 's' : '') + ')</span>';
|
|
86
|
+
el.style.display = 'flex';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderGhThrottleAlert(ghThrottle) {
|
|
90
|
+
const el = document.getElementById('gh-throttle-alert');
|
|
91
|
+
if (!el) return;
|
|
92
|
+
if (!ghThrottle || !ghThrottle.throttled) {
|
|
93
|
+
el.style.display = 'none';
|
|
94
|
+
el.innerHTML = '';
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const resumeTime = new Date(ghThrottle.retryAfter).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
98
|
+
el.innerHTML =
|
|
99
|
+
'<span class="engine-alert-msg">⚠️ GitHub rate-limited — resume ~' + resumeTime + ' (' + ghThrottle.consecutiveHits + ' consecutive hit' + (ghThrottle.consecutiveHits !== 1 ? 's' : '') + ')</span>';
|
|
100
|
+
el.style.display = 'flex';
|
|
101
|
+
}
|
|
102
|
+
|
|
75
103
|
function renderDispatch(dispatch) {
|
|
76
104
|
if (!dispatch) return;
|
|
77
105
|
|
|
@@ -247,4 +275,4 @@ function renderVersionBanner(version) {
|
|
|
247
275
|
}
|
|
248
276
|
}
|
|
249
277
|
|
|
250
|
-
window.MinionsDispatch = { renderEngineStatus, renderEngineAlert, renderVersionBanner, renderDispatch, renderEngineLog, shortTime, showErrorDetails };
|
|
278
|
+
window.MinionsDispatch = { renderEngineStatus, renderEngineAlert, renderAdoThrottleAlert, renderGhThrottleAlert, renderVersionBanner, renderDispatch, renderEngineLog, shortTime, showErrorDetails };
|
package/dashboard/js/settings.js
CHANGED
|
@@ -61,6 +61,7 @@ async function openSettings() {
|
|
|
61
61
|
settingsField('Eval Max Iterations', 'set-evalMaxIterations', e.evalMaxIterations || 3, '', 'Max review→fix cycles before escalating (1-10)') +
|
|
62
62
|
settingsField('Eval Max Cost', 'set-evalMaxCost', e.evalMaxCost === null || e.evalMaxCost === undefined ? '' : e.evalMaxCost, '$', 'USD ceiling per work item across all eval iterations (blank = no limit)') +
|
|
63
63
|
settingsField('Max Build Fix Attempts', 'set-maxBuildFixAttempts', e.maxBuildFixAttempts || 3, '', 'Max auto-fix dispatches per PR before escalating to human (1-10)') +
|
|
64
|
+
settingsField('Agent Busy Reassign', 'set-agentBusyReassignMs', e.agentBusyReassignMs || 600000, 'ms', 'Reassign work to another agent after it waits this long on a busy agent') +
|
|
64
65
|
settingsField('Version Check Interval', 'set-versionCheckInterval', e.versionCheckInterval || 3600000, 'ms', 'How often to check npm for updates (default: 1 hour)') +
|
|
65
66
|
settingsField('Ignored Comment Authors', 'set-ignoredCommentAuthors', (e.ignoredCommentAuthors || []).join(', '), '', 'Comma-separated usernames — comments auto-closed, never trigger fixes') +
|
|
66
67
|
'</div>' +
|
|
@@ -246,6 +247,7 @@ async function saveSettings() {
|
|
|
246
247
|
evalMaxIterations: document.getElementById('set-evalMaxIterations').value,
|
|
247
248
|
evalMaxCost: document.getElementById('set-evalMaxCost').value || null,
|
|
248
249
|
maxBuildFixAttempts: document.getElementById('set-maxBuildFixAttempts').value,
|
|
250
|
+
agentBusyReassignMs: document.getElementById('set-agentBusyReassignMs').value,
|
|
249
251
|
ignoredCommentAuthors: document.getElementById('set-ignoredCommentAuthors').value,
|
|
250
252
|
versionCheckInterval: document.getElementById('set-versionCheckInterval').value,
|
|
251
253
|
ccModel: document.getElementById('set-ccModel').value,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<div class="engine-alert" id="ado-throttle-alert" style="display:none"></div>
|
|
2
|
+
<div class="engine-alert" id="gh-throttle-alert" style="display:none"></div>
|
|
1
3
|
<section>
|
|
2
4
|
<div id="engine-quick-stats" style="display:flex;gap:16px;margin-bottom:12px;font-size:11px;color:var(--muted)"></div>
|
|
3
5
|
</section>
|
package/dashboard.js
CHANGED
|
@@ -22,6 +22,7 @@ const shared = require('./engine/shared');
|
|
|
22
22
|
const queries = require('./engine/queries');
|
|
23
23
|
const teams = require('./engine/teams');
|
|
24
24
|
const ado = require('./engine/ado');
|
|
25
|
+
const gh = require('./engine/github');
|
|
25
26
|
const os = require('os');
|
|
26
27
|
|
|
27
28
|
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, reopenWorkItem } = shared;
|
|
@@ -382,6 +383,8 @@ function getStatus() {
|
|
|
382
383
|
verifyGuides: getVerifyGuides(),
|
|
383
384
|
archivedPrds: getArchivedPrds(),
|
|
384
385
|
engine: { ...getEngineState(), worktreeCount: _countWorktrees() },
|
|
386
|
+
adoThrottle: ado.getAdoThrottleState(),
|
|
387
|
+
ghThrottle: gh.getGhThrottleState(),
|
|
385
388
|
dispatch: getDispatchQueue(),
|
|
386
389
|
engineLog: getEngineLog(),
|
|
387
390
|
metrics: getMetrics(),
|
|
@@ -3751,6 +3754,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3751
3754
|
versionCheckInterval: [60000],
|
|
3752
3755
|
maxBuildFixAttempts: [1, 10],
|
|
3753
3756
|
adoPollStatusEvery: [1], adoPollCommentsEvery: [1],
|
|
3757
|
+
agentBusyReassignMs: [60000],
|
|
3754
3758
|
};
|
|
3755
3759
|
for (const [key, [min, max]] of Object.entries(numericFields)) {
|
|
3756
3760
|
if (e[key] !== undefined) {
|
package/engine/ado.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const shared = require('./shared');
|
|
8
|
-
const { exec, execAsync, getAdoOrgBase, addPrLink, log, ts, dateStamp, PR_STATUS } = shared;
|
|
8
|
+
const { exec, execAsync, getAdoOrgBase, addPrLink, log, ts, dateStamp, PR_STATUS, createThrottleTracker } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
10
|
const { mutateJsonFileLocked } = shared;
|
|
11
11
|
|
|
@@ -47,6 +47,11 @@ function votesToReviewStatus(votes) {
|
|
|
47
47
|
let _adoTokenCache = { token: null, expiresAt: 0 };
|
|
48
48
|
let _adoTokenFailedUntil = 0; // backoff: skip azureauth calls until this timestamp
|
|
49
49
|
|
|
50
|
+
// ─── ADO Throttle State ─────────────────────────────────────────────────────
|
|
51
|
+
// Tracks rate-limiting (HTTP 429/503) from ADO API responses.
|
|
52
|
+
// Uses shared createThrottleTracker factory: backoffMs starts at 60s, doubles, caps at 32 min.
|
|
53
|
+
const _adoThrottle = createThrottleTracker({ label: 'ado', baseBackoffMs: 60000, maxBackoffMs: 32 * 60000 });
|
|
54
|
+
|
|
50
55
|
// ─── Auth Failure Tracking ──────────────────────────────────────────────────
|
|
51
56
|
// Set when pollPrStatus encounters auth errors mid-loop. The engine checks this
|
|
52
57
|
// to bypass the normal 6-tick cadence and re-poll on the next tick.
|
|
@@ -97,6 +102,14 @@ async function adoFetch(url, token, opts = {}) {
|
|
|
97
102
|
signal: AbortSignal.timeout(30000),
|
|
98
103
|
body,
|
|
99
104
|
});
|
|
105
|
+
// ── Throttle detection: intercept 429/503 BEFORE generic !res.ok ──
|
|
106
|
+
if (res.status === 429 || res.status === 503) {
|
|
107
|
+
const retryAfterSec = parseInt(res.headers.get('Retry-After'), 10);
|
|
108
|
+
const retryAfterMs = (retryAfterSec > 0) ? retryAfterSec * 1000 : 0;
|
|
109
|
+
_adoThrottle.recordThrottle(retryAfterMs);
|
|
110
|
+
const state = _adoThrottle.getState();
|
|
111
|
+
throw new Error(`ADO API throttled (${res.status}): retry after ${Math.round((state.retryAfter - Date.now()) / 1000)}s`);
|
|
112
|
+
}
|
|
100
113
|
if (!res.ok) throw new Error(`ADO API ${method} ${res.status}: ${res.statusText}`);
|
|
101
114
|
const text = await res.text();
|
|
102
115
|
if (!text || text.trimStart().startsWith('<')) {
|
|
@@ -110,7 +123,10 @@ async function adoFetch(url, token, opts = {}) {
|
|
|
110
123
|
}
|
|
111
124
|
throw new Error(`ADO returned HTML instead of JSON (likely auth redirect) for ${url.split('?')[0]}`);
|
|
112
125
|
}
|
|
113
|
-
|
|
126
|
+
const json = JSON.parse(text);
|
|
127
|
+
// ── Success decay: decrement consecutiveHits, reset when fully recovered ──
|
|
128
|
+
_adoThrottle.recordSuccess();
|
|
129
|
+
return json;
|
|
114
130
|
}
|
|
115
131
|
|
|
116
132
|
/** Fetch raw text from ADO API (for build logs which aren't JSON). */
|
|
@@ -119,6 +135,14 @@ async function adoFetchText(url, token) {
|
|
|
119
135
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
120
136
|
signal: AbortSignal.timeout(30000),
|
|
121
137
|
});
|
|
138
|
+
// ── Throttle detection: intercept 429/503 BEFORE generic !res.ok ──
|
|
139
|
+
if (res.status === 429 || res.status === 503) {
|
|
140
|
+
const retryAfterSec = parseInt(res.headers.get('Retry-After'), 10);
|
|
141
|
+
const retryAfterMs = (retryAfterSec > 0) ? retryAfterSec * 1000 : 0;
|
|
142
|
+
_adoThrottle.recordThrottle(retryAfterMs);
|
|
143
|
+
const state = _adoThrottle.getState();
|
|
144
|
+
throw new Error(`ADO API throttled (${res.status}): retry after ${Math.round((state.retryAfter - Date.now()) / 1000)}s`);
|
|
145
|
+
}
|
|
122
146
|
if (!res.ok) throw new Error(`ADO API ${res.status}: ${res.statusText}`);
|
|
123
147
|
return res.text();
|
|
124
148
|
}
|
|
@@ -858,6 +882,24 @@ async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
|
858
882
|
};
|
|
859
883
|
}
|
|
860
884
|
|
|
885
|
+
// ─── ADO Throttle Queries ────────────────────────────────────────────────────
|
|
886
|
+
|
|
887
|
+
/** Returns true if ADO is throttled and retryAfter hasn't elapsed. Auto-clears when retryAfter passes. */
|
|
888
|
+
const isAdoThrottled = () => _adoThrottle.isThrottled();
|
|
889
|
+
|
|
890
|
+
/** Returns a snapshot of the current throttle state. Calls isAdoThrottled() for a fresh value. */
|
|
891
|
+
const getAdoThrottleState = () => _adoThrottle.getState();
|
|
892
|
+
|
|
893
|
+
/** Reset throttle state — exported for testing only. */
|
|
894
|
+
function _resetAdoThrottle() {
|
|
895
|
+
_adoThrottle._reset();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** Set throttle state directly — exported for testing only. */
|
|
899
|
+
function _setAdoThrottleForTest(state) {
|
|
900
|
+
_adoThrottle._setForTest(state);
|
|
901
|
+
}
|
|
902
|
+
|
|
861
903
|
module.exports = {
|
|
862
904
|
getAdoToken,
|
|
863
905
|
adoFetch,
|
|
@@ -867,7 +909,11 @@ module.exports = {
|
|
|
867
909
|
checkLiveReviewStatus,
|
|
868
910
|
needsAdoPollRetry,
|
|
869
911
|
isAdoAuthError, // exported for testing
|
|
912
|
+
isAdoThrottled,
|
|
913
|
+
getAdoThrottleState,
|
|
870
914
|
fetchAdoPrMetadata,
|
|
871
915
|
fetchSinglePrBuildStatus,
|
|
916
|
+
_resetAdoThrottle, // exported for testing
|
|
917
|
+
_setAdoThrottleForTest, // exported for testing
|
|
872
918
|
};
|
|
873
919
|
|
package/engine/github.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
|
-
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, mutateJsonFileLocked, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES } = shared;
|
|
8
|
+
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, mutateJsonFileLocked, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, createThrottleTracker } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -68,13 +68,34 @@ function resetSlugBackoff(slug) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// ─── GitHub Rate-Limit Throttle ────────────────────────────────────────────
|
|
72
|
+
// Tracks rate-limiting from GitHub API (gh CLI exits non-zero with rate-limit messages).
|
|
73
|
+
// GitHub rate limits reset hourly, so cap at 60 min.
|
|
74
|
+
const _ghThrottle = createThrottleTracker({ label: 'gh', baseBackoffMs: 60000, maxBackoffMs: 60 * 60000 });
|
|
75
|
+
|
|
76
|
+
/** Returns true if GitHub is rate-limited and retryAfter hasn't elapsed. */
|
|
77
|
+
const isGhThrottled = () => _ghThrottle.isThrottled();
|
|
78
|
+
|
|
79
|
+
/** Returns a snapshot of the current GitHub throttle state. */
|
|
80
|
+
const getGhThrottleState = () => _ghThrottle.getState();
|
|
81
|
+
|
|
71
82
|
/** Run a `gh api` call and parse JSON result. Returns null on failure. */
|
|
72
83
|
async function ghApi(endpoint, slug) {
|
|
73
84
|
try {
|
|
74
85
|
const cmd = `gh api "repos/${slug}${endpoint}"`;
|
|
75
86
|
const result = await execAsync(cmd, { timeout: 30000, encoding: 'utf-8' });
|
|
76
|
-
|
|
87
|
+
const parsed = JSON.parse(result);
|
|
88
|
+
_ghThrottle.recordSuccess();
|
|
89
|
+
return parsed;
|
|
77
90
|
} catch (e) {
|
|
91
|
+
const msg = e?.message || e?.stderr || '';
|
|
92
|
+
// Detect GitHub rate-limit errors from gh CLI output
|
|
93
|
+
if (/rate.?limit|secondary rate|429/i.test(msg)) {
|
|
94
|
+
// Try to extract retry seconds from "N seconds" pattern in error message
|
|
95
|
+
const secMatch = msg.match(/(\d+)\s*seconds?/i);
|
|
96
|
+
const retryAfterMs = secMatch ? parseInt(secMatch[1], 10) * 1000 : 0;
|
|
97
|
+
_ghThrottle.recordThrottle(retryAfterMs);
|
|
98
|
+
}
|
|
78
99
|
log('warn', `GitHub API error (${endpoint}): ${e.message}`);
|
|
79
100
|
return null;
|
|
80
101
|
}
|
|
@@ -731,10 +752,13 @@ module.exports = {
|
|
|
731
752
|
pollPrHumanComments,
|
|
732
753
|
reconcilePrs,
|
|
733
754
|
checkLiveReviewStatus,
|
|
755
|
+
isGhThrottled,
|
|
756
|
+
getGhThrottleState,
|
|
734
757
|
// Exported for testing
|
|
735
758
|
isSlugInBackoff,
|
|
736
759
|
recordSlugFailure,
|
|
737
760
|
resetSlugBackoff,
|
|
738
761
|
_ghPollBackoff,
|
|
762
|
+
_ghThrottle, // exported for testing
|
|
739
763
|
};
|
|
740
764
|
|
package/engine/shared.js
CHANGED
|
@@ -1050,6 +1050,59 @@ function formatTranscriptEntry(t) {
|
|
|
1050
1050
|
return '### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '');
|
|
1051
1051
|
}
|
|
1052
1052
|
|
|
1053
|
+
// ── Throttle Tracker Factory ────────────────────────────────────────────────
|
|
1054
|
+
// Generic rate-limit tracker reusable by both ADO and GitHub integrations.
|
|
1055
|
+
// Returns an object with recordThrottle, recordSuccess, isThrottled, getState.
|
|
1056
|
+
|
|
1057
|
+
function createThrottleTracker({ label, baseBackoffMs = 60000, maxBackoffMs = 32 * 60000 } = {}) {
|
|
1058
|
+
let state = { throttled: false, retryAfter: 0, consecutiveHits: 0, backoffMs: baseBackoffMs };
|
|
1059
|
+
|
|
1060
|
+
function recordThrottle(retryAfterMs) {
|
|
1061
|
+
state.consecutiveHits++;
|
|
1062
|
+
state.backoffMs = Math.min(state.backoffMs * 2, maxBackoffMs);
|
|
1063
|
+
const waitMs = (retryAfterMs > 0) ? retryAfterMs : state.backoffMs;
|
|
1064
|
+
state.throttled = true;
|
|
1065
|
+
state.retryAfter = Date.now() + waitMs;
|
|
1066
|
+
log('warn', `[${label}] Throttled — retry after ${Math.round(waitMs / 1000)}s, consecutive hits: ${state.consecutiveHits}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function recordSuccess() {
|
|
1070
|
+
if (state.consecutiveHits > 0) {
|
|
1071
|
+
state.consecutiveHits--;
|
|
1072
|
+
if (state.consecutiveHits === 0) {
|
|
1073
|
+
state.throttled = false;
|
|
1074
|
+
state.backoffMs = baseBackoffMs;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function isThrottled() {
|
|
1080
|
+
if (!state.throttled) return false;
|
|
1081
|
+
if (Date.now() >= state.retryAfter) {
|
|
1082
|
+
// Auto-clear ALL state when retryAfter has elapsed
|
|
1083
|
+
state.throttled = false;
|
|
1084
|
+
state.backoffMs = baseBackoffMs;
|
|
1085
|
+
state.consecutiveHits = 0;
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
return true;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function getState() {
|
|
1092
|
+
return { throttled: isThrottled(), retryAfter: state.retryAfter, consecutiveHits: state.consecutiveHits };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Testing helpers
|
|
1096
|
+
function _reset() {
|
|
1097
|
+
state = { throttled: false, retryAfter: 0, consecutiveHits: 0, backoffMs: baseBackoffMs };
|
|
1098
|
+
}
|
|
1099
|
+
function _setForTest(overrides) {
|
|
1100
|
+
Object.assign(state, overrides);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return { recordThrottle, recordSuccess, isThrottled, getState, _reset, _setForTest };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1053
1106
|
module.exports = {
|
|
1054
1107
|
MINIONS_DIR,
|
|
1055
1108
|
PR_LINKS_PATH,
|
|
@@ -1113,5 +1166,6 @@ module.exports = {
|
|
|
1113
1166
|
slugify,
|
|
1114
1167
|
formatTranscriptEntry,
|
|
1115
1168
|
_logBuffer, // exported for testing
|
|
1169
|
+
createThrottleTracker,
|
|
1116
1170
|
};
|
|
1117
1171
|
|
package/engine.js
CHANGED
|
@@ -1445,8 +1445,8 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
|
1445
1445
|
// ─── Inbox Consolidation (extracted to engine/consolidation.js) ──────────────
|
|
1446
1446
|
|
|
1447
1447
|
const { consolidateInbox } = require('./engine/consolidation');
|
|
1448
|
-
const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, needsAdoPollRetry, getAdoToken } = require('./engine/ado');
|
|
1449
|
-
const { pollPrStatus: ghPollPrStatus, pollPrHumanComments: ghPollPrHumanComments, reconcilePrs: ghReconcilePrs, checkLiveReviewStatus: ghCheckLiveReview } = require('./engine/github');
|
|
1448
|
+
const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, needsAdoPollRetry, getAdoToken, isAdoThrottled } = require('./engine/ado');
|
|
1449
|
+
const { pollPrStatus: ghPollPrStatus, pollPrHumanComments: ghPollPrHumanComments, reconcilePrs: ghReconcilePrs, checkLiveReviewStatus: ghCheckLiveReview, isGhThrottled } = require('./engine/github');
|
|
1450
1450
|
|
|
1451
1451
|
// ─── State Snapshot ─────────────────────────────────────────────────────────
|
|
1452
1452
|
|
|
@@ -3130,11 +3130,15 @@ async function tickInner() {
|
|
|
3130
3130
|
// Awaited so PR state is consistent before discoverWork reads it
|
|
3131
3131
|
// Also re-polls early if previous tick had ADO auth failures (stale build status recovery)
|
|
3132
3132
|
if (tickCount % adoPollStatusEvery === 0 || needsAdoPollRetry()) {
|
|
3133
|
-
if (adoPollEnabled) {
|
|
3133
|
+
if (adoPollEnabled && !isAdoThrottled()) {
|
|
3134
3134
|
try { await pollPrStatus(config); } catch (err) { log('warn', `ADO PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
|
|
3135
|
+
} else if (adoPollEnabled && isAdoThrottled()) {
|
|
3136
|
+
log('info', '[ado] PR status poll skipped — throttled');
|
|
3135
3137
|
}
|
|
3136
|
-
if (ghPollEnabled) {
|
|
3138
|
+
if (ghPollEnabled && !isGhThrottled()) {
|
|
3137
3139
|
try { await ghPollPrStatus(config); } catch (err) { log('warn', `GitHub PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
|
|
3140
|
+
} else if (ghPollEnabled && isGhThrottled()) {
|
|
3141
|
+
log('info', '[gh] PR status poll skipped — throttled');
|
|
3138
3142
|
}
|
|
3139
3143
|
try { await processPendingRebases(config); } catch (err) { log('warn', `Pending rebase processing error: ${err?.message || err}`); }
|
|
3140
3144
|
// Sync PR status back to PRD items (missing → done when active PR exists)
|
|
@@ -3157,11 +3161,15 @@ async function tickInner() {
|
|
|
3157
3161
|
|
|
3158
3162
|
// 2.7. Poll PR threads for human comments (every adoPollCommentsEvery ticks, default ~12 minutes)
|
|
3159
3163
|
if (tickCount % adoPollCommentsEvery === 0) {
|
|
3160
|
-
if (adoPollEnabled) {
|
|
3164
|
+
if (adoPollEnabled && !isAdoThrottled()) {
|
|
3161
3165
|
try { await pollPrHumanComments(config); } catch (err) { log('warn', `ADO PR comment poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
|
|
3166
|
+
} else if (adoPollEnabled && isAdoThrottled()) {
|
|
3167
|
+
log('info', '[ado] PR comment poll skipped — throttled');
|
|
3162
3168
|
}
|
|
3163
|
-
if (ghPollEnabled) {
|
|
3169
|
+
if (ghPollEnabled && !isGhThrottled()) {
|
|
3164
3170
|
try { await ghPollPrHumanComments(config); } catch (err) { log('warn', `GitHub PR comment poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
|
|
3171
|
+
} else if (ghPollEnabled && isGhThrottled()) {
|
|
3172
|
+
log('info', '[gh] PR comment poll skipped — throttled');
|
|
3165
3173
|
}
|
|
3166
3174
|
// Reconciliation runs regardless of poll flags — it's a recovery sweep, not a convenience poll
|
|
3167
3175
|
try { await reconcilePrs(config); } catch (err) { log('warn', `ADO PR reconciliation error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.956",
|
|
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"
|