@yemi33/minions 0.1.2377 → 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 +43 -55
- package/engine/queries.js +11 -3
- 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
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
|
-
// detects a symlink is treated as "skip" — the input is effectively
|
|
3018
|
-
// not tracked. (Round-2 review finding #7.)
|
|
3019
|
-
const s = fs.lstatSync(fp);
|
|
3020
|
-
if (s.isSymbolicLink()) continue;
|
|
3021
|
-
if (s.mtimeMs > max) max = s.mtimeMs;
|
|
3022
|
-
// Directory inputs: dir mtime only advances on add/remove of
|
|
3023
|
-
// immediate entries, not on in-place file edits. Walk two levels
|
|
3024
|
-
// down so editing prd/<plan>.json in place (depth 1) and
|
|
3025
|
-
// agents/<id>/live-output.log (depth 2) both bust the ETag.
|
|
3026
|
-
// Steering edits at agents/<id>/inbox/steering-*.md (depth 3)
|
|
3027
|
-
// currently rely on add/remove cadence rather than per-file content
|
|
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;
|
|
3057
3017
|
}
|
|
3058
|
-
|
|
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;
|
|
3028
|
+
}
|
|
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);
|
|
@@ -7677,7 +7665,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
7677
7665
|
h = Math.imul(h ^ ((e.size || 0) >>> 0), 16777619);
|
|
7678
7666
|
h = Math.imul(h ^ Math.floor(e.sortTs || 0), 16777619);
|
|
7679
7667
|
}
|
|
7680
|
-
const sweepMtime = _maxInputMtimeMs([
|
|
7668
|
+
const sweepMtime = await _maxInputMtimeMs([
|
|
7681
7669
|
path.join(ENGINE_DIR, 'kb-swept.json'),
|
|
7682
7670
|
path.join(ENGINE_DIR, 'kb-sweep-state.json'),
|
|
7683
7671
|
]);
|
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/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"
|