@yemi33/minions 0.1.2060 → 0.1.2062
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/render-agents.js +39 -24
- package/engine/queries.js +76 -1
- package/package.json +1 -1
|
@@ -69,24 +69,18 @@ function renderAgents(agents) {
|
|
|
69
69
|
`).join('');
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Emoji, name and role are all user-controlled fields; routing them through textContent
|
|
80
|
-
// guarantees no HTML interpretation even if the escape function were ever bypassed.
|
|
72
|
+
// Re-render the detail-panel header (name + emoji + runtime/model chips +
|
|
73
|
+
// status badge + lastAction + blocking-tool / resultSummary blocks) from a
|
|
74
|
+
// slim agentData record. Extracted so renderAgents can call it every poll
|
|
75
|
+
// tick to keep an open detail panel live without re-fetching the expensive
|
|
76
|
+
// /api/agent/<id> charter/history/output payload. SEC-03 Phase A rules
|
|
77
|
+
// still apply: emoji/name/role go through textContent.
|
|
78
|
+
function _renderAgentDetailHeader(agent) {
|
|
81
79
|
const nameEl = document.getElementById('detail-agent-name');
|
|
80
|
+
if (!nameEl) return;
|
|
82
81
|
const emojiSpan = document.createElement('span');
|
|
83
82
|
emojiSpan.style.fontSize = '22px';
|
|
84
83
|
emojiSpan.textContent = agent.emoji || '';
|
|
85
|
-
// Runtime tag \u2014 uses the inline-SVG logo from the same RUNTIME_TAGS map the
|
|
86
|
-
// card uses, so the visual is consistent. The container's user-controlled
|
|
87
|
-
// text fields stay on the textContent path; the SVG is a hardcoded literal
|
|
88
|
-
// from RUNTIME_TAGS keyed by the runtime string (server-controlled, finite
|
|
89
|
-
// set), so injecting via innerHTML on the icon-only span is safe.
|
|
90
84
|
const runtimeMeta = RUNTIME_TAGS[agent.runtime];
|
|
91
85
|
const runtimeSpan = document.createElement('span');
|
|
92
86
|
runtimeSpan.title = 'Runtime: ' + (runtimeMeta?.label || agent.runtime || 'unknown');
|
|
@@ -100,9 +94,6 @@ async function openAgentDetail(id) {
|
|
|
100
94
|
runtimeSpan.style.cssText += ';font-size:10px;font-weight:600;letter-spacing:0.4px;text-transform:uppercase;padding:2px 6px;border:1px solid var(--muted);border-radius:3px;color:var(--muted)';
|
|
101
95
|
runtimeSpan.textContent = agent.runtime || 'unknown';
|
|
102
96
|
}
|
|
103
|
-
// W-mpmwxk4y00053271 — mirror the model chip the card shows so the detail
|
|
104
|
-
// header stays in sync. textContent path keeps the model string from being
|
|
105
|
-
// interpreted as HTML.
|
|
106
97
|
const modelSpan = (agent.model && typeof agent.model === 'string')
|
|
107
98
|
? (() => {
|
|
108
99
|
const s = document.createElement('span');
|
|
@@ -121,12 +112,27 @@ async function openAgentDetail(id) {
|
|
|
121
112
|
nameEl.replaceChildren(...children);
|
|
122
113
|
|
|
123
114
|
const badgeClass = agent.status;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
115
|
+
const statusEl = document.getElementById('detail-status-line');
|
|
116
|
+
if (statusEl) {
|
|
117
|
+
// 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)
|
|
118
|
+
statusEl.innerHTML =
|
|
119
|
+
'<span class="status-badge ' + badgeClass + '">' + agent.status.toUpperCase() + '</span> ' +
|
|
120
|
+
'<span style="color:var(--muted)">' + escapeHtml(agent.lastAction) + '</span>' +
|
|
121
|
+
(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:11px;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>' : '') +
|
|
122
|
+
(agent.resultSummary ? '<div style="margin-top:4px;font-size:11px;color:var(--text);line-height:1.4">' + renderMd(agent.resultSummary.slice(0, 300)) + '</div>' : '');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function openAgentDetail(id) {
|
|
127
|
+
const agent = agentData.find(a => a.id === id);
|
|
128
|
+
if (!agent) return;
|
|
129
|
+
currentAgentId = id;
|
|
130
|
+
currentTab = (agent.status === 'working') ? 'live' : 'thought-process';
|
|
131
|
+
|
|
132
|
+
// SEC-03 Phase A: Build the detail header via DOM + textContent instead of innerHTML.
|
|
133
|
+
// Emoji, name and role are all user-controlled fields; routing them through textContent
|
|
134
|
+
// guarantees no HTML interpretation even if the escape function were ever bypassed.
|
|
135
|
+
_renderAgentDetailHeader(agent);
|
|
130
136
|
|
|
131
137
|
// Show panel immediately with loading state — don't wait for API
|
|
132
138
|
document.getElementById('detail-content').innerHTML = '<div style="padding:24px;text-align:center;color:var(--muted)">Loading...</div>';
|
|
@@ -161,7 +167,12 @@ function _tickAgentRuntimes() {
|
|
|
161
167
|
el.textContent = 'Running: ' + (hr > 0 ? hr + 'h ' : '') + min + 'm ' + sec + 's';
|
|
162
168
|
});
|
|
163
169
|
}
|
|
164
|
-
// Start ticker after each render if working agents exist
|
|
170
|
+
// Start ticker after each render if working agents exist, and refresh the
|
|
171
|
+
// open agent-detail panel header so status / lastAction / blocking-tool /
|
|
172
|
+
// resultSummary track the latest /api/status slice without re-fetching the
|
|
173
|
+
// expensive /api/agent/<id> charter/history/output payload. The body tabs
|
|
174
|
+
// (thought-process, live, output, etc.) are loaded once on open via
|
|
175
|
+
// openAgentDetail's safeFetch — they stay as-is until the user reopens.
|
|
165
176
|
var _origRenderAgents = renderAgents;
|
|
166
177
|
renderAgents = function(agents) {
|
|
167
178
|
_origRenderAgents(agents);
|
|
@@ -170,6 +181,10 @@ renderAgents = function(agents) {
|
|
|
170
181
|
_tickAgentRuntimes();
|
|
171
182
|
_agentRuntimeTimer = setInterval(_tickAgentRuntimes, 1000);
|
|
172
183
|
}
|
|
184
|
+
if (currentAgentId) {
|
|
185
|
+
var open = agents.find(function(a) { return a.id === currentAgentId; });
|
|
186
|
+
if (open) _renderAgentDetailHeader(open);
|
|
187
|
+
}
|
|
173
188
|
};
|
|
174
189
|
|
|
175
190
|
window.MinionsAgents = { renderAgents, openAgentDetail };
|
package/engine/queries.js
CHANGED
|
@@ -789,12 +789,24 @@ let _skillsCacheKey = null;
|
|
|
789
789
|
let _skillIndexCache = null;
|
|
790
790
|
let _skillIndexCacheTs = 0;
|
|
791
791
|
let _skillIndexCacheKey = null;
|
|
792
|
+
// Top-level cache for getSkills (the read+parse + meta-build path layered on
|
|
793
|
+
// top of collectSkillFiles). Without this, every /api/status slow-state
|
|
794
|
+
// rebuild re-read and re-parsed all ~80 SKILL.md files (~29ms warm even
|
|
795
|
+
// though collectSkillFiles itself was cached) because getSkills did the
|
|
796
|
+
// I/O on the cached file list. Shares the same 30s TTL + cacheKey so an
|
|
797
|
+
// invalidateSkillsCache() call still flushes everything.
|
|
798
|
+
let _getSkillsResultCache = null;
|
|
799
|
+
let _getSkillsResultCacheTs = 0;
|
|
800
|
+
let _getSkillsResultCacheKey = null;
|
|
792
801
|
const SKILLS_CACHE_TTL = 30000; // 30s — skill files change rarely (agent extraction, manual authoring)
|
|
793
802
|
|
|
794
803
|
function invalidateSkillsCache() {
|
|
795
804
|
_skillsCache = null;
|
|
796
805
|
_skillsCacheTs = 0;
|
|
797
806
|
_skillsCacheKey = null;
|
|
807
|
+
_getSkillsResultCache = null;
|
|
808
|
+
_getSkillsResultCacheTs = 0;
|
|
809
|
+
_getSkillsResultCacheKey = null;
|
|
798
810
|
_skillIndexCache = null;
|
|
799
811
|
_skillIndexCacheTs = 0;
|
|
800
812
|
_skillIndexCacheKey = null;
|
|
@@ -906,6 +918,18 @@ const SKILL_SOURCE_BY_SCOPE = {
|
|
|
906
918
|
};
|
|
907
919
|
|
|
908
920
|
function getSkills(config) {
|
|
921
|
+
// Cache the entire read+parse round, not just the file list. /api/status
|
|
922
|
+
// slow-state calls getSkills on every rebuild and re-reading 80+ SKILL.md
|
|
923
|
+
// files + re-parsing their YAML frontmatter cost ~29ms each time (the
|
|
924
|
+
// single biggest hot-path offender after getPrdInfo cold). 30s TTL +
|
|
925
|
+
// shared cacheKey with collectSkillFiles so invalidateSkillsCache() still
|
|
926
|
+
// flushes everything in one shot.
|
|
927
|
+
const now = Date.now();
|
|
928
|
+
config = config || getConfig();
|
|
929
|
+
const cacheKey = _skillsCacheKeyFor(config, os.homedir());
|
|
930
|
+
if (_getSkillsResultCache && _getSkillsResultCacheKey === cacheKey && (now - _getSkillsResultCacheTs) < SKILLS_CACHE_TTL) {
|
|
931
|
+
return _getSkillsResultCache;
|
|
932
|
+
}
|
|
909
933
|
const all = [];
|
|
910
934
|
for (const { file: f, dir, scope, projectName, skillName } of collectSkillFiles(config)) {
|
|
911
935
|
try {
|
|
@@ -923,6 +947,9 @@ function getSkills(config) {
|
|
|
923
947
|
});
|
|
924
948
|
} catch { /* optional */ }
|
|
925
949
|
}
|
|
950
|
+
_getSkillsResultCache = all;
|
|
951
|
+
_getSkillsResultCacheTs = now;
|
|
952
|
+
_getSkillsResultCacheKey = cacheKey;
|
|
926
953
|
return all;
|
|
927
954
|
}
|
|
928
955
|
|
|
@@ -2143,8 +2170,34 @@ function resetProjectGitStatusCache() {
|
|
|
2143
2170
|
* so a 5-project fleet runs 14 statSync calls per cache miss — bounded
|
|
2144
2171
|
* and unmeasurable in benchmarks.
|
|
2145
2172
|
*/
|
|
2173
|
+
// Cache the slow-state path list: it only changes when projects are added/
|
|
2174
|
+
// removed or runtimes are registered/deregistered (essentially process-
|
|
2175
|
+
// lifetime stable in practice). Without it we walked runtime-registry +
|
|
2176
|
+
// projects + resolved per-project gitdir/commondir on every /api/status
|
|
2177
|
+
// rebuild — ~7.5ms per call. The fast-state list is cheaper (~2ms) but
|
|
2178
|
+
// we cache it too for symmetry. Cache key is keyed off the project list
|
|
2179
|
+
// shape so a new project (rare event, goes through configMutation +
|
|
2180
|
+
// invalidateStatusCache) invalidates correctly.
|
|
2181
|
+
let _fastMtimePathsCache = null;
|
|
2182
|
+
let _fastMtimePathsCacheKey = null;
|
|
2183
|
+
let _slowMtimePathsCache = null;
|
|
2184
|
+
let _slowMtimePathsCacheKey = null;
|
|
2185
|
+
function _mtimePathsCacheKey(config) {
|
|
2186
|
+
const projects = getProjects(config).map(p => (p.name || '') + '|' + (p.localPath || ''));
|
|
2187
|
+
return projects.join(';');
|
|
2188
|
+
}
|
|
2189
|
+
function _invalidateMtimePathsCache() {
|
|
2190
|
+
_fastMtimePathsCache = null;
|
|
2191
|
+
_fastMtimePathsCacheKey = null;
|
|
2192
|
+
_slowMtimePathsCache = null;
|
|
2193
|
+
_slowMtimePathsCacheKey = null;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2146
2196
|
function getStatusFastStateMtimePaths(config) {
|
|
2147
|
-
|
|
2197
|
+
config = config || getConfig();
|
|
2198
|
+
const cacheKey = _mtimePathsCacheKey(config);
|
|
2199
|
+
if (_fastMtimePathsCache && _fastMtimePathsCacheKey === cacheKey) return _fastMtimePathsCache;
|
|
2200
|
+
const projects = getProjects(config);
|
|
2148
2201
|
const files = [
|
|
2149
2202
|
// Engine-level state surfaced by getDispatchQueue. `control.json`,
|
|
2150
2203
|
// `log.json`, and `metrics.json` are intentionally omitted — see the
|
|
@@ -2234,6 +2287,8 @@ function getStatusFastStateMtimePaths(config) {
|
|
|
2234
2287
|
files.push(path.join(commonGitDir, 'FETCH_HEAD'));
|
|
2235
2288
|
}
|
|
2236
2289
|
}
|
|
2290
|
+
_fastMtimePathsCache = files;
|
|
2291
|
+
_fastMtimePathsCacheKey = cacheKey;
|
|
2237
2292
|
return files;
|
|
2238
2293
|
}
|
|
2239
2294
|
|
|
@@ -2307,6 +2362,8 @@ function getStatusFastStateMtimePaths(config) {
|
|
|
2307
2362
|
*/
|
|
2308
2363
|
function getStatusSlowStateMtimePaths(config) {
|
|
2309
2364
|
config = config || getConfig();
|
|
2365
|
+
const cacheKey = _mtimePathsCacheKey(config);
|
|
2366
|
+
if (_slowMtimePathsCache && _slowMtimePathsCacheKey === cacheKey) return _slowMtimePathsCache;
|
|
2310
2367
|
const projects = getProjects(config);
|
|
2311
2368
|
const homeDir = os.homedir();
|
|
2312
2369
|
const files = [
|
|
@@ -2328,6 +2385,15 @@ function getStatusSlowStateMtimePaths(config) {
|
|
|
2328
2385
|
// invalidateStatusCache({includeSlow:true}); tracker entry catches any
|
|
2329
2386
|
// CLI/editor edit that bypasses the API.
|
|
2330
2387
|
path.join(MINIONS_DIR, 'pinned.md'),
|
|
2388
|
+
// work-items.json — central + per-project files (per-project pushed below
|
|
2389
|
+
// alongside the PR paths). The PRD progress slice in slow-state is
|
|
2390
|
+
// *derived* from work-item statuses via getPrdInfo's input-hash, so a
|
|
2391
|
+
// WI flipping dispatched→done changes prdProgress without touching any
|
|
2392
|
+
// file in this tracker. Without this entry the slow-state cache hangs
|
|
2393
|
+
// on stale PRD progress for up to 60s after a WI completes (user
|
|
2394
|
+
// report: visit /plans, see all items active; switch to /home + hard
|
|
2395
|
+
// refresh; the WIs are done; return to /plans, now items show as done).
|
|
2396
|
+
path.join(MINIONS_DIR, 'work-items.json'),
|
|
2331
2397
|
];
|
|
2332
2398
|
|
|
2333
2399
|
// Skill discovery roots (surfaced by _buildStatusSlowState → getSkills).
|
|
@@ -2378,8 +2444,17 @@ function getStatusSlowStateMtimePaths(config) {
|
|
|
2378
2444
|
const commonGitDir = _resolveCommonGitDir(gitDir);
|
|
2379
2445
|
files.push(path.join(gitDir, 'logs', 'HEAD'));
|
|
2380
2446
|
files.push(path.join(commonGitDir, 'FETCH_HEAD'));
|
|
2447
|
+
// Per-project work-items.json + pull-requests.json — same reason as the
|
|
2448
|
+
// central work-items.json above: the prdProgress slice is derived from
|
|
2449
|
+
// their contents via getPrdInfo's input hash. A WI completion in a
|
|
2450
|
+
// project that uses its own work-items.json file would otherwise hang
|
|
2451
|
+
// slow-state until TTL.
|
|
2452
|
+
try { files.push(projectWorkItemsPath(project)); } catch { /* path helper optional */ }
|
|
2453
|
+
try { files.push(projectPrPath(project)); } catch { /* path helper optional */ }
|
|
2381
2454
|
}
|
|
2382
2455
|
|
|
2456
|
+
_slowMtimePathsCache = files;
|
|
2457
|
+
_slowMtimePathsCacheKey = cacheKey;
|
|
2383
2458
|
return files;
|
|
2384
2459
|
}
|
|
2385
2460
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2062",
|
|
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"
|