@yemi33/minions 0.1.2376 → 0.1.2378
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/dashboard/js/refresh.js +8 -5
- package/dashboard/js/render-agents.js +50 -7
- package/dashboard/js/settings.js +23 -30
- package/dashboard.js +64 -85
- package/engine/llm.js +2 -1
- package/engine/model-discovery.js +15 -2
- package/engine/queries.js +11 -3
- package/engine/runtimes/codex.js +81 -7
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -130,7 +130,8 @@ function _detectPageChanges(data) {
|
|
|
130
130
|
// for the same input. Cross-restart safety lives in the dashboardBuildId
|
|
131
131
|
// reload path below — RENDER_VERSIONS handles the within-process case.
|
|
132
132
|
const RENDER_VERSIONS = {
|
|
133
|
-
|
|
133
|
+
// Bumped 2→3 for client-ticked relative last-action timestamps.
|
|
134
|
+
agents: 3,
|
|
134
135
|
prdProgress: 2,
|
|
135
136
|
prdPrs: 1,
|
|
136
137
|
inbox: 2,
|
|
@@ -516,17 +517,19 @@ function _processStatusUpdate(data, opts) {
|
|
|
516
517
|
// the /api/work-items + /api/pull-requests .then handlers
|
|
517
518
|
// (_fireCrossSliceRender) where the FRESH fetched lists are available.
|
|
518
519
|
// Agents now come from /api/agents — a dedicated fresh-JSON endpoint with
|
|
519
|
-
// input-mtime ETag (issue #2949).
|
|
520
|
+
// input-mtime ETag (issue #2949). Conditional requests keep unchanged polls
|
|
521
|
+
// from reparsing the roster and rebuilding every card/timer.
|
|
520
522
|
// cmdUpdateAgentList consumes the same slice but renders the command-line
|
|
521
523
|
// chip pop-up so we feed it the fresh array too.
|
|
522
524
|
_safeRender('agents', function() {
|
|
523
525
|
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
524
526
|
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
525
527
|
window._lastRequestedSeq.agents = seq;
|
|
526
|
-
|
|
527
|
-
.then(function (
|
|
528
|
-
.then(function (fresh) {
|
|
528
|
+
_condFetchJson('/api/agents')
|
|
529
|
+
.then(function (resp) {
|
|
529
530
|
if (seq < (window._lastRequestedSeq.agents || 0)) return;
|
|
531
|
+
if (resp.notModified) return;
|
|
532
|
+
const fresh = resp.data;
|
|
530
533
|
const list = Array.isArray(fresh) ? fresh : window._lastAgents;
|
|
531
534
|
if (list) window._lastAgents = list;
|
|
532
535
|
_safeRender('agents', function() { renderAgents(list); });
|
|
@@ -44,6 +44,34 @@ function _modelChipHtml(model) {
|
|
|
44
44
|
return '<span class="agent-model-tag" title="Model: ' + escapeHtml(model) + '" style="font-size:var(--text-xs);font-weight:600;letter-spacing:0.4px;padding:1px 5px;margin-left:4px;border:1px solid var(--muted);border-radius:3px;color:var(--muted);background:transparent">' + escapeHtml(model) + '</span>';
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function _agentHasTimedAction(agent) {
|
|
48
|
+
return !!(agent && agent.lastActionPrefix && Number.isFinite(Number(agent.lastActionAt)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _formatAgentAction(prefix, actionAt, fallback) {
|
|
52
|
+
var actionAtMs = Number(actionAt);
|
|
53
|
+
var text = prefix && Number.isFinite(actionAtMs)
|
|
54
|
+
? String(prefix) + ' (' + timeAgo(actionAtMs) + ')'
|
|
55
|
+
: String(fallback || '');
|
|
56
|
+
return text.length > 120 ? text.slice(0, 120) + '...' : text;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _agentLastActionText(agent) {
|
|
60
|
+
return _formatAgentAction(agent.lastActionPrefix, agent.lastActionAt, agent.lastAction);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _agentTimedActionAttrs(agent) {
|
|
64
|
+
return _agentHasTimedAction(agent)
|
|
65
|
+
? ' data-action-prefix="' + escapeHtml(agent.lastActionPrefix) + '" data-action-at="' + Number(agent.lastActionAt) + '"'
|
|
66
|
+
: '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _agentActionHtml(agent) {
|
|
70
|
+
var text = _agentLastActionText(agent);
|
|
71
|
+
var attrs = _agentTimedActionAttrs(agent);
|
|
72
|
+
return '<div class="agent-action"' + attrs + ' title="' + escapeHtml(text) + '">' + escapeHtml(text) + '</div>';
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
function renderAgents(agents) {
|
|
48
76
|
agentData = agents;
|
|
49
77
|
const grid = document.getElementById('agents-grid');
|
|
@@ -56,7 +84,7 @@ function renderAgents(agents) {
|
|
|
56
84
|
<span class="status-badge ${escapeHtml(a.status)}">${escapeHtml(a.status)}</span>
|
|
57
85
|
</div>
|
|
58
86
|
<div class="agent-role">${escapeHtml(a.role)}</div>
|
|
59
|
-
|
|
87
|
+
${_agentActionHtml(a)}
|
|
60
88
|
${(function() {
|
|
61
89
|
var s = a.started_at, c = a.completed_at;
|
|
62
90
|
if (s && c) { var d = new Date(c) - new Date(s); if (d > 0) { var sec = Math.floor(d/1000)%60, min = Math.floor(d/60000)%60, hr = Math.floor(d/3600000); return '<div style="font-size:var(--text-xs);color:var(--muted)">Last run: ' + (hr > 0 ? hr + 'h ' : '') + min + 'm ' + sec + 's</div>'; } }
|
|
@@ -144,7 +172,7 @@ async function openAgentDetail(id) {
|
|
|
144
172
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; status is an internal bounded enum and all user data is wrapped in escapeHtml()/renderMd() (fields: lastAction, blocking tool, resultSummary)
|
|
145
173
|
document.getElementById('detail-status-line').innerHTML =
|
|
146
174
|
'<span class="status-badge ' + badgeClass + '">' + agent.status.toUpperCase() + '</span> ' +
|
|
147
|
-
'<span style="color:var(--muted)">' + escapeHtml(agent
|
|
175
|
+
'<span class="agent-detail-action"' + _agentTimedActionAttrs(agent) + ' style="color:var(--muted)">' + escapeHtml(_agentLastActionText(agent)) + '</span>' +
|
|
148
176
|
(agent._blockingToolCall ? '<div style="margin-top:4px;padding:4px 8px;background:rgba(130,160,210,0.13);border:1px solid rgba(130,160,210,0.3);border-radius:4px;font-size:var(--text-base);color:var(--muted)">⏳ Blocking tool call (' + escapeHtml(agent._blockingToolCall.tool) + ') — silent ' + Math.round(agent._blockingToolCall.silentMs/60000) + 'min, timeout in ' + Math.round(agent._blockingToolCall.remainingMs/60000) + 'min</div>' : '') +
|
|
149
177
|
(agent.resultSummary ? '<div style="margin-top:4px;font-size:var(--text-base);color:var(--text);line-height:1.4">' + renderMd(agent.resultSummary.slice(0, 300)) + '</div>' : '');
|
|
150
178
|
|
|
@@ -168,11 +196,19 @@ async function openAgentDetail(id) {
|
|
|
168
196
|
|
|
169
197
|
}
|
|
170
198
|
|
|
171
|
-
// Tick
|
|
199
|
+
// Tick wall-clock-derived card text every second.
|
|
172
200
|
var _agentRuntimeTimer = null;
|
|
173
201
|
function _tickAgentRuntimes() {
|
|
174
|
-
var
|
|
175
|
-
|
|
202
|
+
var grid = document.getElementById('agents-grid');
|
|
203
|
+
var els = grid ? grid.querySelectorAll('.agent-runtime-tick') : [];
|
|
204
|
+
var actionEls = grid ? Array.from(grid.querySelectorAll('.agent-action[data-action-at]')) : [];
|
|
205
|
+
var detailStatus = document.getElementById('detail-status-line');
|
|
206
|
+
var detailAction = detailStatus && detailStatus.querySelector('.agent-detail-action[data-action-at]');
|
|
207
|
+
if (detailAction) actionEls.push(detailAction);
|
|
208
|
+
if (els.length === 0 && actionEls.length === 0) {
|
|
209
|
+
if (_agentRuntimeTimer) { clearInterval(_agentRuntimeTimer); _agentRuntimeTimer = null; }
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
176
212
|
var now = Date.now();
|
|
177
213
|
els.forEach(function(el) {
|
|
178
214
|
var ms = now - new Date(el.dataset.started).getTime();
|
|
@@ -180,13 +216,20 @@ function _tickAgentRuntimes() {
|
|
|
180
216
|
var sec = Math.floor(ms / 1000) % 60, min = Math.floor(ms / 60000) % 60, hr = Math.floor(ms / 3600000);
|
|
181
217
|
el.textContent = 'Running: ' + (hr > 0 ? hr + 'h ' : '') + min + 'm ' + sec + 's';
|
|
182
218
|
});
|
|
219
|
+
actionEls.forEach(function(el) {
|
|
220
|
+
var text = _formatAgentAction(el.dataset.actionPrefix, Number(el.dataset.actionAt), el.textContent);
|
|
221
|
+
el.textContent = text;
|
|
222
|
+
el.title = text;
|
|
223
|
+
});
|
|
183
224
|
}
|
|
184
|
-
// Start ticker after each render if
|
|
225
|
+
// Start ticker after each render if any card has wall-clock-derived text.
|
|
185
226
|
var _origRenderAgents = renderAgents;
|
|
186
227
|
renderAgents = function(agents) {
|
|
187
228
|
_origRenderAgents(agents);
|
|
188
229
|
if (_agentRuntimeTimer) { clearInterval(_agentRuntimeTimer); _agentRuntimeTimer = null; }
|
|
189
|
-
if (agents.some(function(a) {
|
|
230
|
+
if (agents.some(function(a) {
|
|
231
|
+
return (a.status === 'working' && a.started_at) || _agentHasTimedAction(a);
|
|
232
|
+
})) {
|
|
190
233
|
_tickAgentRuntimes();
|
|
191
234
|
_agentRuntimeTimer = setInterval(_tickAgentRuntimes, 1000);
|
|
192
235
|
}
|
package/dashboard/js/settings.js
CHANGED
|
@@ -890,13 +890,11 @@ async function initRuntimeFleetUI(engineCfg, agentsCfg) {
|
|
|
890
890
|
}
|
|
891
891
|
|
|
892
892
|
/**
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
* Claude or model-discovery disabled). The "Default (CLI chooses)" option is
|
|
896
|
-
* always present and submits empty string.
|
|
893
|
+
* Render an editable model field backed by discovered-model suggestions.
|
|
894
|
+
* Discovery is advisory: staged/future/custom model IDs must remain typeable.
|
|
897
895
|
*/
|
|
898
|
-
async function loadModelsForRuntime(runtimeName, inputId, currentValue) {
|
|
899
|
-
const wrap = document.getElementById(inputId)?.parentElement;
|
|
896
|
+
async function loadModelsForRuntime(runtimeName, inputId, currentValue, forceRefresh) {
|
|
897
|
+
const wrap = document.getElementById(inputId + '-wrap') || document.getElementById(inputId)?.parentElement;
|
|
900
898
|
if (!wrap) return;
|
|
901
899
|
const token = _nextModelLoadToken('runtime', inputId);
|
|
902
900
|
if (!runtimeName) {
|
|
@@ -906,42 +904,37 @@ async function loadModelsForRuntime(runtimeName, inputId, currentValue) {
|
|
|
906
904
|
}
|
|
907
905
|
let payload = { models: null };
|
|
908
906
|
try {
|
|
909
|
-
const
|
|
907
|
+
const suffix = forceRefresh ? '/models/refresh' : '/models';
|
|
908
|
+
const res = await fetch('/api/runtimes/' + encodeURIComponent(runtimeName) + suffix, forceRefresh ? { method: 'POST' } : undefined);
|
|
910
909
|
if (res.ok) payload = await res.json();
|
|
911
910
|
} catch { /* fall through to free-text */ }
|
|
912
911
|
|
|
913
912
|
if (!_isCurrentModelLoad('runtime', inputId, token)) return;
|
|
914
913
|
const models = Array.isArray(payload.models) ? payload.models : null;
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: currentValue; inputId is an internal fixed DOM id)
|
|
919
|
-
wrap.innerHTML = '<input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="Default (CLI chooses)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">';
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
// Dropdown. The first option submits empty string → "Default (CLI chooses)".
|
|
923
|
-
let opts = '<option value=""' + (!currentValue ? ' selected' : '') + '>Default (CLI chooses)</option>';
|
|
924
|
-
for (const m of models) {
|
|
914
|
+
const listId = inputId + '-options';
|
|
915
|
+
let opts = '';
|
|
916
|
+
for (const m of (models || [])) {
|
|
925
917
|
const id = m.id || m.name || '';
|
|
926
918
|
if (!id) continue;
|
|
927
|
-
const label = m.name && m.name !== id ?
|
|
928
|
-
opts += '<option value="' + escHtml(id) + '"' + (
|
|
919
|
+
const label = m.name && m.name !== id ? m.name : '';
|
|
920
|
+
opts += '<option value="' + escHtml(id) + '"' + (label ? ' label="' + escHtml(label) + '"' : '') + '></option>';
|
|
929
921
|
}
|
|
930
|
-
|
|
931
|
-
//
|
|
932
|
-
|
|
933
|
-
|
|
922
|
+
const title = forceRefresh ? 'Model list refreshed' : 'Refresh models from the selected CLI';
|
|
923
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml()
|
|
924
|
+
wrap.innerHTML = '<div style="display:flex;gap:4px"><input id="' + inputId + '" list="' + listId + '" value="' + escHtml(currentValue || '') + '" placeholder="Default (CLI chooses)" autocomplete="off" style="min-width:0;flex:1;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)"><button type="button" data-model-refresh title="' + escHtml(title) + '" aria-label="Refresh model list" style="padding:3px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);cursor:pointer">↻</button></div><datalist id="' + listId + '">' + opts + '</datalist>';
|
|
925
|
+
const refresh = wrap.querySelector('[data-model-refresh]');
|
|
926
|
+
if (refresh) {
|
|
927
|
+
refresh.addEventListener('click', async function() {
|
|
928
|
+
const liveValue = document.getElementById(inputId)?.value || '';
|
|
929
|
+
refresh.disabled = true;
|
|
930
|
+
await loadModelsForRuntime(runtimeName, inputId, liveValue, true);
|
|
931
|
+
});
|
|
934
932
|
}
|
|
935
|
-
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: model id, model label, currentValue; inputId is an internal fixed DOM id)
|
|
936
|
-
wrap.innerHTML = '<select id="' + inputId + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' + opts + '</select>';
|
|
937
933
|
}
|
|
938
934
|
|
|
939
935
|
/**
|
|
940
|
-
* Per-agent model hydrator.
|
|
941
|
-
*
|
|
942
|
-
* given runtime. Output element keeps `data-agent` + `data-field="model"` so
|
|
943
|
-
* the existing save flow picks it up unchanged. Free-text input fallback
|
|
944
|
-
* when the runtime returns no model list (Claude / discovery disabled).
|
|
936
|
+
* Per-agent model hydrator. The catalog is a datalist rather than a closed
|
|
937
|
+
* select so newly released and custom IDs stay available.
|
|
945
938
|
*/
|
|
946
939
|
async function loadModelsForAgent(agentId, runtimeName, currentValue) {
|
|
947
940
|
const cell = document.querySelector('[data-runtime-model="' + agentId + '"]');
|
package/dashboard.js
CHANGED
|
@@ -2996,68 +2996,56 @@ function _stateReadContentType(ext) {
|
|
|
2996
2996
|
// issue #2949 documented. This pattern bypasses both:
|
|
2997
2997
|
//
|
|
2998
2998
|
// 1. Caller supplies an array of input file paths.
|
|
2999
|
-
// 2. We
|
|
2999
|
+
// 2. We asynchronously stat each one and take MAX(mtimeMs). That's the ETag.
|
|
3000
3000
|
// 3. If the client's If-None-Match matches → 304 with no body.
|
|
3001
3001
|
// 4. Otherwise call `builder()` to assemble the response from scratch
|
|
3002
3002
|
// (no outer cache layer to go stale).
|
|
3003
3003
|
// 5. Stamp ETag + Content-Type and send.
|
|
3004
3004
|
//
|
|
3005
|
-
// Cost per request when ETag matches: N
|
|
3006
|
-
// Cost on a miss: same
|
|
3005
|
+
// Cost per request when ETag matches: N asynchronous stat calls.
|
|
3006
|
+
// Cost on a miss: the same stat walk + the builder, which is exactly the
|
|
3007
3007
|
// existing queries.getAgents() / getMetrics() / etc. that /api/status
|
|
3008
3008
|
// already pays. Net change: same compute, no caching, always-fresh.
|
|
3009
|
-
function
|
|
3010
|
-
let
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
// mtime — accept this lag rather than walking unbounded.
|
|
3029
|
-
// (Review finding #6.)
|
|
3030
|
-
if (s.isDirectory()) {
|
|
3031
|
-
let level1;
|
|
3032
|
-
try { level1 = fs.readdirSync(fp); } catch { level1 = []; }
|
|
3033
|
-
for (const name of level1) {
|
|
3034
|
-
const child = path.join(fp, name);
|
|
3035
|
-
let cs;
|
|
3036
|
-
try { cs = fs.lstatSync(child); } catch { continue; }
|
|
3037
|
-
if (cs.isSymbolicLink()) continue;
|
|
3038
|
-
if (cs.mtimeMs > max) max = cs.mtimeMs;
|
|
3039
|
-
if (cs.isDirectory()) {
|
|
3040
|
-
let level2;
|
|
3041
|
-
try { level2 = fs.readdirSync(child); } catch { level2 = []; }
|
|
3042
|
-
for (const inner of level2) {
|
|
3043
|
-
try {
|
|
3044
|
-
const is = fs.lstatSync(path.join(child, inner));
|
|
3045
|
-
if (is.isSymbolicLink()) continue;
|
|
3046
|
-
if (is.mtimeMs > max) max = is.mtimeMs;
|
|
3047
|
-
} catch { /* skip unreadable */ }
|
|
3048
|
-
}
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
} catch {
|
|
3053
|
-
// Missing input = not yet on disk. Don't bust the ETag — a builder
|
|
3054
|
-
// that legitimately depends on the file will surface its own empty
|
|
3055
|
-
// state, and the ETag will move once the file appears.
|
|
3056
|
-
}
|
|
3009
|
+
async function _freshnessEntryMtimeMs(fp, depth) {
|
|
3010
|
+
let stat;
|
|
3011
|
+
try {
|
|
3012
|
+
// lstat avoids following broken Windows junctions or circular symlinks,
|
|
3013
|
+
// which can incur long OS timeouts. State inputs never require symlinks.
|
|
3014
|
+
stat = await fs.promises.lstat(fp);
|
|
3015
|
+
} catch {
|
|
3016
|
+
return 0;
|
|
3017
|
+
}
|
|
3018
|
+
if (stat.isSymbolicLink()) return 0;
|
|
3019
|
+
|
|
3020
|
+
let max = stat.mtimeMs;
|
|
3021
|
+
if (!stat.isDirectory() || depth <= 0) return max;
|
|
3022
|
+
|
|
3023
|
+
let names;
|
|
3024
|
+
try {
|
|
3025
|
+
names = await fs.promises.readdir(fp);
|
|
3026
|
+
} catch {
|
|
3027
|
+
return max;
|
|
3057
3028
|
}
|
|
3058
|
-
|
|
3029
|
+
const childMtimes = await Promise.all(
|
|
3030
|
+
names.map(name => _freshnessEntryMtimeMs(path.join(fp, name), depth - 1)),
|
|
3031
|
+
);
|
|
3032
|
+
for (const childMtime of childMtimes) {
|
|
3033
|
+
if (childMtime > max) max = childMtime;
|
|
3034
|
+
}
|
|
3035
|
+
return max;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
async function _maxInputMtimeMs(inputs) {
|
|
3039
|
+
// Directory mtime only advances on add/remove, not in-place edits. Walk two
|
|
3040
|
+
// levels so prd/<plan>.json and agents/<id>/live-output.log both bust ETags.
|
|
3041
|
+
// Deeper steering edits still rely on add/remove cadence, matching the prior
|
|
3042
|
+
// bounded walk.
|
|
3043
|
+
const mtimes = await Promise.all(
|
|
3044
|
+
inputs.map(fp => _freshnessEntryMtimeMs(fp, 2)),
|
|
3045
|
+
);
|
|
3046
|
+
return Math.floor(mtimes.reduce((max, mtime) => Math.max(max, mtime), 0));
|
|
3059
3047
|
}
|
|
3060
|
-
function serveFreshJson(req, res, opts) {
|
|
3048
|
+
async function serveFreshJson(req, res, opts) {
|
|
3061
3049
|
const inputs = Array.isArray(opts && opts.inputs) ? opts.inputs : [];
|
|
3062
3050
|
const builder = opts && typeof opts.builder === 'function' ? opts.builder : null;
|
|
3063
3051
|
const tag = opts && opts.tag ? String(opts.tag) : 'v';
|
|
@@ -3067,7 +3055,7 @@ function serveFreshJson(req, res, opts) {
|
|
|
3067
3055
|
res.end(JSON.stringify({ error: 'serveFreshJson: builder is required' }));
|
|
3068
3056
|
return;
|
|
3069
3057
|
}
|
|
3070
|
-
const mtime = _maxInputMtimeMs(inputs);
|
|
3058
|
+
const mtime = await _maxInputMtimeMs(inputs);
|
|
3071
3059
|
const eventVersion = _getCurrentEventVersion();
|
|
3072
3060
|
const etag = '"' + tag + '-' + mtime + '-' + eventVersion + '"';
|
|
3073
3061
|
res.setHeader('ETag', etag);
|
|
@@ -4502,6 +4490,7 @@ async function _preflightModelCheck({ runtime: cliOverride, model: modelOverride
|
|
|
4502
4490
|
const resolvedModel = llm._resolveModelForRuntime(adapter, { model: modelOverride, engineConfig });
|
|
4503
4491
|
if (!resolvedModel) return null;
|
|
4504
4492
|
if (!adapter.capabilities || adapter.capabilities.modelDiscovery !== true) return null;
|
|
4493
|
+
if (adapter.capabilities.strictModelCatalog === false) return null;
|
|
4505
4494
|
|
|
4506
4495
|
let list;
|
|
4507
4496
|
try {
|
|
@@ -7676,7 +7665,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
7676
7665
|
h = Math.imul(h ^ ((e.size || 0) >>> 0), 16777619);
|
|
7677
7666
|
h = Math.imul(h ^ Math.floor(e.sortTs || 0), 16777619);
|
|
7678
7667
|
}
|
|
7679
|
-
const sweepMtime = _maxInputMtimeMs([
|
|
7668
|
+
const sweepMtime = await _maxInputMtimeMs([
|
|
7680
7669
|
path.join(ENGINE_DIR, 'kb-swept.json'),
|
|
7681
7670
|
path.join(ENGINE_DIR, 'kb-sweep-state.json'),
|
|
7682
7671
|
]);
|
|
@@ -10572,8 +10561,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10572
10561
|
});
|
|
10573
10562
|
writeCcEvent(envelope);
|
|
10574
10563
|
liveState.donePayload = envelope;
|
|
10575
|
-
_ccStreamEnded = true;
|
|
10576
10564
|
if (liveState.endResponse) liveState.endResponse();
|
|
10565
|
+
_ccStreamEnded = true;
|
|
10577
10566
|
_scheduleCcLiveCleanup(tabId);
|
|
10578
10567
|
_logCcStreamEnd(_ccTelemetry, 'error-preflight-model-unavailable', { runtime: preflightFailure.runtime });
|
|
10579
10568
|
return;
|
|
@@ -10621,8 +10610,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10621
10610
|
if (initial.missingRuntime) return initial;
|
|
10622
10611
|
|
|
10623
10612
|
// Handle failure — non-zero exit with text = max_turns or partial success, still usable
|
|
10624
|
-
if (!initial.text && wasResume && initial.code !== 0
|
|
10625
|
-
// Resume failed (stale/expired session) — auto-retry as fresh
|
|
10613
|
+
if (!initial.text && wasResume && initial.code !== 0) {
|
|
10614
|
+
// Resume failed (stale/expired session) — auto-retry as fresh. A
|
|
10615
|
+
// briefly disconnected client can reattach to the live turn.
|
|
10626
10616
|
console.log(`[CC-stream] Resume failed (code=${initial.code}) — retrying fresh`);
|
|
10627
10617
|
const freshPreamble = buildCCStatePreamble();
|
|
10628
10618
|
const freshCarryover = _buildTranscriptCarryover(body.transcript, { currentMessage: body.message });
|
|
@@ -10672,11 +10662,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10672
10662
|
// treating an empty `text` as a failure. A genuinely empty turn has
|
|
10673
10663
|
// both text and raw empty.
|
|
10674
10664
|
if ((!result.text && !result.raw) || result.error) {
|
|
10675
|
-
if (req.destroyed) {
|
|
10676
|
-
_ccStreamEnded = true;
|
|
10677
|
-
_logCcStreamEnd(_ccTelemetry, 'llm-empty-client-gone', { code: result.code });
|
|
10678
|
-
return;
|
|
10679
|
-
}
|
|
10680
10665
|
// W-mpmwxni2000c25c7-b — surface the typed error envelope as a
|
|
10681
10666
|
// distinct SSE `event: error` frame so the client renders a real
|
|
10682
10667
|
// error UI (with a retry hint derived from `retriable`) instead of
|
|
@@ -11316,20 +11301,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11316
11301
|
knownForResolved = new Set(list.models.map(m => m.id || m.name).filter(Boolean));
|
|
11317
11302
|
}
|
|
11318
11303
|
} catch { /* unknown runtime */ }
|
|
11319
|
-
if (knownForResolved &&
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
} catch { /* skip */ }
|
|
11332
|
-
}
|
|
11304
|
+
if (knownForResolved && knownForResolved.has(runtimeModelStr)) return null;
|
|
11305
|
+
// Catalogs lag staged releases and local/custom providers. Unknown is
|
|
11306
|
+
// therefore allowed, but a model positively published by a different
|
|
11307
|
+
// runtime remains an actionable incompatible-combination error.
|
|
11308
|
+
for (const rt of _engineRuntimes.listRuntimes()) {
|
|
11309
|
+
if (rt === resolvedRuntime) continue;
|
|
11310
|
+
try {
|
|
11311
|
+
const otherList = await _engineModelDiscovery.getRuntimeModels(rt, { config });
|
|
11312
|
+
if (Array.isArray(otherList?.models) && otherList.models.some(m => (m.id || m.name) === modelStr)) {
|
|
11313
|
+
return `belongs to runtime "${rt}" but resolved runtime is "${resolvedRuntime}" — incompatible combination`;
|
|
11314
|
+
}
|
|
11315
|
+
} catch { /* skip */ }
|
|
11333
11316
|
}
|
|
11334
11317
|
return null;
|
|
11335
11318
|
}
|
|
@@ -11486,15 +11469,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11486
11469
|
const resolvedCli = config.agents[id].cli || config.engine.defaultCli || 'copilot';
|
|
11487
11470
|
const runtimeModelStr = _resolveModelForRuntime(candidate, resolvedCli);
|
|
11488
11471
|
const knownModels = await _modelsFor(resolvedCli);
|
|
11489
|
-
//
|
|
11490
|
-
//
|
|
11491
|
-
//
|
|
11492
|
-
// model belongs to a DIFFERENT runtime's list — that's how
|
|
11493
|
-
// we catch claude+gpt-5.5 (gpt-5.5 is in Copilot's list).
|
|
11472
|
+
// Published membership is a positive signal, not a closed enum:
|
|
11473
|
+
// staged releases/custom providers can legitimately be absent.
|
|
11474
|
+
// Still reject IDs positively owned by a different runtime.
|
|
11494
11475
|
let rejection = null;
|
|
11495
|
-
if (knownModels
|
|
11496
|
-
rejection = `not a valid model for runtime "${resolvedCli}" (known: ${[...knownModels].slice(0, 4).join(', ')}${knownModels.size > 4 ? '…' : ''})`;
|
|
11497
|
-
} else if (!knownModels) {
|
|
11476
|
+
if (!knownModels || !knownModels.has(runtimeModelStr)) {
|
|
11498
11477
|
const owner = await _ownerOfModel(candidate);
|
|
11499
11478
|
if (owner && owner !== resolvedCli) {
|
|
11500
11479
|
rejection = `belongs to runtime "${owner}" but agent uses "${resolvedCli}" — incompatible combination`;
|
package/engine/llm.js
CHANGED
|
@@ -591,8 +591,9 @@ function _createStreamAccumulator({
|
|
|
591
591
|
if (!id || !onToolUpdate) return;
|
|
592
592
|
onToolUpdate(id, status);
|
|
593
593
|
},
|
|
594
|
-
toolUseAlreadySeen(name, input) {
|
|
594
|
+
toolUseAlreadySeen(name, input, id = null) {
|
|
595
595
|
if (!name) return false;
|
|
596
|
+
if (id) return toolUses.some(t => t.id === id);
|
|
596
597
|
const stringified = JSON.stringify(input || {});
|
|
597
598
|
return toolUses.some(t => t.name === name && JSON.stringify(t.input) === stringified);
|
|
598
599
|
},
|
|
@@ -99,6 +99,13 @@ async function getRuntimeModels(runtimeName, { force = false, ttlMs = MODEL_CACH
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const cachePath = adapter.modelsCache || null;
|
|
102
|
+
// Runtime catalogs are versioned with the CLI that produced them. Codex in
|
|
103
|
+
// particular ships new bundled model IDs with CLI releases, so a one-hour
|
|
104
|
+
// time-only cache otherwise hides new models immediately after an upgrade.
|
|
105
|
+
let modelCacheKey = null;
|
|
106
|
+
if (typeof adapter.getModelCacheKey === 'function') {
|
|
107
|
+
try { modelCacheKey = await adapter.getModelCacheKey(); } catch { /* optional hint */ }
|
|
108
|
+
}
|
|
102
109
|
|
|
103
110
|
// Cache hit path — only on non-forced reads. We accept any cached payload
|
|
104
111
|
// whose `cachedAt` parses to a valid timestamp within the TTL window;
|
|
@@ -110,7 +117,8 @@ async function getRuntimeModels(runtimeName, { force = false, ttlMs = MODEL_CACH
|
|
|
110
117
|
const ts = Date.parse(cached.cachedAt);
|
|
111
118
|
if (Number.isFinite(ts)) {
|
|
112
119
|
const age = Date.now() - ts;
|
|
113
|
-
|
|
120
|
+
const versionMatches = !modelCacheKey || cached.modelCacheKey === modelCacheKey;
|
|
121
|
+
if (age >= 0 && age < ttlMs && versionMatches) {
|
|
114
122
|
const models = Array.isArray(cached.models) ? cached.models : null;
|
|
115
123
|
return { runtime: runtimeName, models, cachedAt: cached.cachedAt };
|
|
116
124
|
}
|
|
@@ -134,7 +142,12 @@ async function getRuntimeModels(runtimeName, { force = false, ttlMs = MODEL_CACH
|
|
|
134
142
|
|
|
135
143
|
const cachedAt = new Date().toISOString();
|
|
136
144
|
if (cachePath) {
|
|
137
|
-
_writeCacheFile(cachePath, {
|
|
145
|
+
_writeCacheFile(cachePath, {
|
|
146
|
+
runtime: runtimeName,
|
|
147
|
+
models,
|
|
148
|
+
cachedAt,
|
|
149
|
+
...(modelCacheKey ? { modelCacheKey } : {}),
|
|
150
|
+
});
|
|
138
151
|
}
|
|
139
152
|
return { runtime: runtimeName, models, cachedAt };
|
|
140
153
|
}
|
package/engine/queries.js
CHANGED
|
@@ -758,6 +758,8 @@ function getAgents(config) {
|
|
|
758
758
|
const s = getAgentStatus(a.id); // derives from dispatch.json
|
|
759
759
|
|
|
760
760
|
let lastAction = 'Waiting for assignment';
|
|
761
|
+
let lastActionPrefix = null;
|
|
762
|
+
let lastActionAt = null;
|
|
761
763
|
// Show the dispatch task as a stable label for the whole run — don't swap
|
|
762
764
|
// in the transient per-tool-call `_runningToolDescription`, which reads like
|
|
763
765
|
// a task change to operators (e.g. an agent's own "Wait until temp dirs
|
|
@@ -768,17 +770,23 @@ function getAgents(config) {
|
|
|
768
770
|
else if (s.status === 'error') lastAction = `Error: ${s.task}`;
|
|
769
771
|
else if (steeringInboxFiles.length > 0) {
|
|
770
772
|
const lastSteer = steeringInboxFiles[steeringInboxFiles.length - 1];
|
|
771
|
-
|
|
773
|
+
lastActionPrefix = `Pending steering: ${lastSteer.file}`;
|
|
774
|
+
lastActionAt = lastSteer.createdAtMs;
|
|
775
|
+
lastAction = `${lastActionPrefix} (${timeSince(lastActionAt)})`;
|
|
772
776
|
}
|
|
773
777
|
else if (inboxFiles.length > 0) {
|
|
774
778
|
const lastOutput = path.join(INBOX_DIR, inboxFiles[inboxFiles.length - 1]);
|
|
775
|
-
try {
|
|
779
|
+
try {
|
|
780
|
+
lastActionPrefix = `Output: ${path.basename(lastOutput)}`;
|
|
781
|
+
lastActionAt = fs.statSync(lastOutput).mtimeMs;
|
|
782
|
+
lastAction = `${lastActionPrefix} (${timeSince(lastActionAt)})`;
|
|
783
|
+
} catch { /* optional */ }
|
|
776
784
|
}
|
|
777
785
|
|
|
778
786
|
const chartered = fs.existsSync(path.join(AGENTS_DIR, a.id, 'charter.md'));
|
|
779
787
|
if (lastAction.length > 120) lastAction = lastAction.slice(0, 120) + '...';
|
|
780
788
|
return {
|
|
781
|
-
...a, runtime, model, status: s.status, lastAction,
|
|
789
|
+
...a, runtime, model, status: s.status, lastAction, lastActionPrefix, lastActionAt,
|
|
782
790
|
// Descriptive capability tags; tolerate legacy `skills` configs by
|
|
783
791
|
// normalizing to `expertise` for the dashboard/settings UI.
|
|
784
792
|
expertise: a.expertise ?? a.skills ?? [],
|
package/engine/runtimes/codex.js
CHANGED
|
@@ -320,6 +320,17 @@ function _normalizeConfigOverrides(configOverrides) {
|
|
|
320
320
|
return [];
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
const _MIME_EXT = {
|
|
324
|
+
'image/png': 'png',
|
|
325
|
+
'image/jpeg': 'jpg',
|
|
326
|
+
'image/gif': 'gif',
|
|
327
|
+
'image/webp': 'webp',
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
function _mimeToExt(mimeType) {
|
|
331
|
+
return _MIME_EXT[String(mimeType || '').toLowerCase()] || 'bin';
|
|
332
|
+
}
|
|
333
|
+
|
|
323
334
|
function buildArgs(opts = {}) {
|
|
324
335
|
const {
|
|
325
336
|
model,
|
|
@@ -329,11 +340,13 @@ function buildArgs(opts = {}) {
|
|
|
329
340
|
sandbox = 'workspace-write',
|
|
330
341
|
skipGitRepoCheck = true,
|
|
331
342
|
configOverrides,
|
|
343
|
+
images,
|
|
344
|
+
tmpDir,
|
|
332
345
|
} = opts;
|
|
333
346
|
|
|
334
347
|
const args = ['exec'];
|
|
335
|
-
|
|
336
|
-
|
|
348
|
+
// These are `exec` options, not `exec resume` options. Codex's resume
|
|
349
|
+
// parser rejects flags such as --color and --sandbox after the subcommand.
|
|
337
350
|
args.push('--json', '--color', 'never');
|
|
338
351
|
if (model) args.push('--model', String(model));
|
|
339
352
|
if (sandbox) args.push('--sandbox', String(sandbox));
|
|
@@ -347,6 +360,20 @@ function buildArgs(opts = {}) {
|
|
|
347
360
|
args.push('--config', override);
|
|
348
361
|
}
|
|
349
362
|
|
|
363
|
+
if (sessionId) args.push('resume', String(sessionId));
|
|
364
|
+
|
|
365
|
+
// Codex accepts repeatable --image paths on fresh and resumed turns. The
|
|
366
|
+
// call-scoped tmpDir is removed by engine/llm.js after the process exits.
|
|
367
|
+
if (Array.isArray(images) && images.length && tmpDir) {
|
|
368
|
+
for (let i = 0; i < images.length; i++) {
|
|
369
|
+
const img = images[i];
|
|
370
|
+
if (!img || !img.dataBase64) continue;
|
|
371
|
+
const filePath = path.join(tmpDir, `img-${i}.${_mimeToExt(img.mimeType)}`);
|
|
372
|
+
fs.writeFileSync(filePath, Buffer.from(img.dataBase64, 'base64'));
|
|
373
|
+
args.push('--image', filePath);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
350
377
|
args.push('-');
|
|
351
378
|
return args;
|
|
352
379
|
}
|
|
@@ -544,6 +571,21 @@ function _extractToolUse(obj) {
|
|
|
544
571
|
const itemType = String(item?.type || '');
|
|
545
572
|
if (!/tool|function|command|web_search/i.test(type) && !/tool|command|web_search/i.test(itemType)) return null;
|
|
546
573
|
const data = item || obj.data || obj;
|
|
574
|
+
const id = _pickString(data.id, obj?.item_id, obj?.itemId, obj?.data?.item_id);
|
|
575
|
+
const rawStatus = String(data.status || obj?.status || '').toLowerCase();
|
|
576
|
+
const status = /fail|error|cancel/.test(rawStatus) ? 'failed'
|
|
577
|
+
: /complete|success/.test(rawStatus) ? 'completed'
|
|
578
|
+
: null;
|
|
579
|
+
|
|
580
|
+
if (/command_execution/i.test(itemType)) {
|
|
581
|
+
const command = _pickString(data.command, data.input);
|
|
582
|
+
if (!command) return null;
|
|
583
|
+
return { name: 'Bash', input: { command }, id: id || null, status };
|
|
584
|
+
}
|
|
585
|
+
if (/web_search/i.test(itemType)) {
|
|
586
|
+
const query = _pickString(data.query, data.input, data.arguments);
|
|
587
|
+
return { name: 'WebSearch', input: query ? { query } : {}, id: id || null, status };
|
|
588
|
+
}
|
|
547
589
|
const name = _pickString(
|
|
548
590
|
data.tool_name,
|
|
549
591
|
data.toolName,
|
|
@@ -555,7 +597,7 @@ function _extractToolUse(obj) {
|
|
|
555
597
|
);
|
|
556
598
|
if (!name) return null;
|
|
557
599
|
const input = data.arguments || data.args || data.input || data.function?.arguments || {};
|
|
558
|
-
return { name, input };
|
|
600
|
+
return { name, input, id: id || null, status };
|
|
559
601
|
}
|
|
560
602
|
|
|
561
603
|
function _usageFrom(obj) {
|
|
@@ -675,6 +717,17 @@ function parseError(rawOutput) {
|
|
|
675
717
|
if (!text) return { message: '', code: null, retriable: true };
|
|
676
718
|
const lower = text.toLowerCase();
|
|
677
719
|
|
|
720
|
+
if (/requires a newer version of codex|upgrade to the latest (?:app or )?cli|codex (?:cli )?is (?:too old|out of date)/i.test(text)) {
|
|
721
|
+
const name = _extractInvalidModelName(text)
|
|
722
|
+
|| (text.match(/['"`]([A-Za-z0-9._\/-]+)['"`]/)?.[1])
|
|
723
|
+
|| 'configured model';
|
|
724
|
+
return {
|
|
725
|
+
message: `The installed Codex CLI is too old for model "${name}". Upgrade Codex, refresh the model list, and try again.`,
|
|
726
|
+
code: 'config-error',
|
|
727
|
+
retriable: false,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
678
731
|
const hasAuth = /not authenticated|login required|please.*log.*in|api key.*invalid|invalid api key|\bunauthorized\b|\b401\b|\b403\b/i.test(text);
|
|
679
732
|
if (hasAuth) {
|
|
680
733
|
return { message: 'Codex authentication failed - run `codex login` or configure an OpenAI API key, then try again.', code: 'auth-failure', retriable: false };
|
|
@@ -773,6 +826,20 @@ async function listModels({ env = process.env, timeoutMs = 10000 } = {}) {
|
|
|
773
826
|
}
|
|
774
827
|
}
|
|
775
828
|
|
|
829
|
+
function getModelCacheKey({ env = process.env, timeoutMs = 10000 } = {}) {
|
|
830
|
+
const resolved = resolveBinary({ env });
|
|
831
|
+
if (!resolved) return null;
|
|
832
|
+
try {
|
|
833
|
+
return _execFileCapture(resolved.bin, [...(resolved.leadingArgs || []), '--version'], {
|
|
834
|
+
env,
|
|
835
|
+
timeoutMs,
|
|
836
|
+
native: resolved.native,
|
|
837
|
+
}).trim() || null;
|
|
838
|
+
} catch {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
776
843
|
function createStreamConsumer(ctx) {
|
|
777
844
|
let deltaText = '';
|
|
778
845
|
let terminalText = '';
|
|
@@ -789,7 +856,11 @@ function createStreamConsumer(ctx) {
|
|
|
789
856
|
if (/reasoning|thinking/i.test(type)) ctx.notifyThinking();
|
|
790
857
|
|
|
791
858
|
const toolUse = _extractToolUse(obj);
|
|
792
|
-
if (toolUse)
|
|
859
|
+
if (toolUse) {
|
|
860
|
+
const seen = ctx.toolUseAlreadySeen(toolUse.name, toolUse.input, toolUse.id);
|
|
861
|
+
if (!seen) ctx.pushToolUse(toolUse.name, toolUse.input, toolUse.id);
|
|
862
|
+
if (toolUse.id && toolUse.status) ctx.updateToolUse(toolUse.id, toolUse.status);
|
|
863
|
+
}
|
|
793
864
|
|
|
794
865
|
const delta = _extractDelta(obj);
|
|
795
866
|
if (delta) {
|
|
@@ -823,6 +894,9 @@ const capabilities = {
|
|
|
823
894
|
costTracking: false,
|
|
824
895
|
modelShorthands: false,
|
|
825
896
|
modelDiscovery: true,
|
|
897
|
+
// Bundled catalogs can lag staged/custom model availability. Let Codex
|
|
898
|
+
// validate unknown IDs so it can return its precise version/provider error.
|
|
899
|
+
strictModelCatalog: false,
|
|
826
900
|
promptViaArg: false,
|
|
827
901
|
budgetCap: false,
|
|
828
902
|
bareMode: false,
|
|
@@ -830,9 +904,7 @@ const capabilities = {
|
|
|
830
904
|
sessionPersistenceControl: false,
|
|
831
905
|
resumePromptCarryover: true,
|
|
832
906
|
streamConsumer: true,
|
|
833
|
-
|
|
834
|
-
// P-3f8a1c92). Engine code gates on this flag, never on runtime.name.
|
|
835
|
-
imageInput: false,
|
|
907
|
+
imageInput: true,
|
|
836
908
|
};
|
|
837
909
|
|
|
838
910
|
const INSTALL_HINT = 'install Codex CLI with `npm install -g @openai/codex` or `brew install --cask codex`, then run `codex login`.';
|
|
@@ -844,6 +916,7 @@ module.exports = {
|
|
|
844
916
|
resolveBinary,
|
|
845
917
|
capsFile: CAPS_FILE,
|
|
846
918
|
listModels,
|
|
919
|
+
getModelCacheKey,
|
|
847
920
|
modelsCache: MODELS_CACHE,
|
|
848
921
|
spawnScript: path.join(ENGINE_DIR, 'spawn-agent.js'),
|
|
849
922
|
installHint: INSTALL_HINT,
|
|
@@ -878,5 +951,6 @@ module.exports = {
|
|
|
878
951
|
_extractModelsFromCatalog,
|
|
879
952
|
_readCatalogIds,
|
|
880
953
|
_extractInvalidModelName,
|
|
954
|
+
_extractToolUse,
|
|
881
955
|
CAPS_SCHEMA_VERSION,
|
|
882
956
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2378",
|
|
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"
|