@yemi33/minions 0.1.2151 → 0.1.2153
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 +44 -11
- package/dashboard/js/render-kb.js +12 -7
- package/dashboard.js +27 -0
- package/engine/lifecycle.js +89 -0
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -214,6 +214,30 @@ function _changed(key, value, version) {
|
|
|
214
214
|
return true;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
// Conditional JSON fetch for the heavy per-page endpoints (browser-OOM fix).
|
|
218
|
+
// /api/knowledge (~4.5MB) and /api/work-items (~4.2MB) were re-fetched AND
|
|
219
|
+
// re-parsed on every 4s refresh tick — ~11MB of allocation churn per tick that
|
|
220
|
+
// OOM-crashed the tab during active foreground use. These endpoints emit an
|
|
221
|
+
// input-mtime ETag server-side (serveFreshJson + handleKnowledgeList), so send
|
|
222
|
+
// If-None-Match off the last-seen ETag and resolve { notModified:true } on a
|
|
223
|
+
// 304. Callers skip the parse + render entirely and keep their last-rendered
|
|
224
|
+
// state, so an idle dashboard drops from ~11MB/tick to ~0. Routes through
|
|
225
|
+
// safeFetch (like the /api/status poll it mirrors) for the 15s abort timeout +
|
|
226
|
+
// X-Minions-Dashboard-* tab/visibility correlation headers. Per-URL ETag cache;
|
|
227
|
+
// a non-304 error rejects so each call site's existing .catch arm still fires.
|
|
228
|
+
var _condFetchEtags = {};
|
|
229
|
+
function _condFetchJson(url) {
|
|
230
|
+
var headers = {};
|
|
231
|
+
if (_condFetchEtags[url]) headers['If-None-Match'] = _condFetchEtags[url];
|
|
232
|
+
return safeFetch(url, { headers }).then(function (r) {
|
|
233
|
+
if (r.status === 304) return { notModified: true, data: null };
|
|
234
|
+
if (!r.ok) return Promise.reject(new Error('HTTP ' + r.status));
|
|
235
|
+
var etag = r.headers && r.headers.get ? r.headers.get('etag') : null;
|
|
236
|
+
if (etag) _condFetchEtags[url] = etag;
|
|
237
|
+
return r.json().then(function (data) { return { notModified: false, data: data }; });
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
217
241
|
function _formatCcPowerLabel(autoMode) {
|
|
218
242
|
var runtime = autoMode && autoMode.ccCli ? String(autoMode.ccCli) : 'claude';
|
|
219
243
|
var runtimeLabel = runtime.charAt(0).toUpperCase() + runtime.slice(1);
|
|
@@ -518,7 +542,11 @@ function _processStatusUpdate(data, opts) {
|
|
|
518
542
|
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
519
543
|
window._lastRequestedSeq.prd = seq;
|
|
520
544
|
Promise.all([
|
|
521
|
-
fetch(
|
|
545
|
+
// Conditional fetch (browser-OOM fix): flatten to null on a 304 or error,
|
|
546
|
+
// which the cached status/progress fallback below already handles. The
|
|
547
|
+
// .catch keeps a prd failure from rejecting the whole Promise.all and
|
|
548
|
+
// starving the verify-guides slice (matches the old `r.ok ? … : null`).
|
|
549
|
+
_condFetchJson('/api/prd').then(function (resp) { return resp.notModified ? null : resp.data; }).catch(function () { return null; }),
|
|
522
550
|
fetch('/api/verify-guides').then(function (r) { return r.ok ? r.json() : null; }),
|
|
523
551
|
])
|
|
524
552
|
.then(function (results) {
|
|
@@ -616,10 +644,12 @@ function _processStatusUpdate(data, opts) {
|
|
|
616
644
|
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
617
645
|
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
618
646
|
window._lastRequestedSeq.prs = seq;
|
|
619
|
-
|
|
620
|
-
.then(function (
|
|
621
|
-
.then(function (fresh) {
|
|
647
|
+
_condFetchJson('/api/pull-requests')
|
|
648
|
+
.then(function (resp) {
|
|
622
649
|
if (seq < (window._lastRequestedSeq.prs || 0)) return;
|
|
650
|
+
// 304 → PR list unchanged; keep last-rendered state + cross-slice sigs.
|
|
651
|
+
if (resp.notModified) return;
|
|
652
|
+
const fresh = resp.data;
|
|
623
653
|
const list = Array.isArray(fresh) ? fresh : (window._lastPullRequests || []);
|
|
624
654
|
const wasFirstNonEmpty = !Array.isArray(window._lastPullRequests);
|
|
625
655
|
window._lastPullRequests = list;
|
|
@@ -695,11 +725,12 @@ function _processStatusUpdate(data, opts) {
|
|
|
695
725
|
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
696
726
|
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
697
727
|
window._lastRequestedSeq.dispatch = seq;
|
|
698
|
-
|
|
699
|
-
.then(function (
|
|
700
|
-
.then(function (fresh) {
|
|
728
|
+
_condFetchJson('/api/dispatch')
|
|
729
|
+
.then(function (resp) {
|
|
701
730
|
if (seq < (window._lastRequestedSeq.dispatch || 0)) return;
|
|
702
|
-
|
|
731
|
+
// 304 → dispatch queue unchanged; keep last-rendered panes + scroll.
|
|
732
|
+
if (resp.notModified) return;
|
|
733
|
+
const d = resp.data || window._lastDispatch;
|
|
703
734
|
if (d) window._lastDispatch = d;
|
|
704
735
|
_safeRender('dispatch', function() { renderDispatch(d, dispatchOpts); });
|
|
705
736
|
// Dispatch is the third cross-slice edge: derivePlanStatus reads
|
|
@@ -785,10 +816,12 @@ function _processStatusUpdate(data, opts) {
|
|
|
785
816
|
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
786
817
|
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
787
818
|
window._lastRequestedSeq.workItems = seq;
|
|
788
|
-
|
|
789
|
-
.then(function (
|
|
790
|
-
.then(function (fresh) {
|
|
819
|
+
_condFetchJson('/api/work-items')
|
|
820
|
+
.then(function (resp) {
|
|
791
821
|
if (seq < (window._lastRequestedSeq.workItems || 0)) return;
|
|
822
|
+
// 304 → list unchanged; keep the last-rendered rows + DOM untouched.
|
|
823
|
+
if (resp.notModified) return;
|
|
824
|
+
const fresh = resp.data;
|
|
792
825
|
const list = Array.isArray(fresh) ? fresh : (window._lastWorkItems || []);
|
|
793
826
|
window._lastWorkItems = list;
|
|
794
827
|
_safeRender('workItems', function() { renderWorkItems(list); });
|
|
@@ -32,13 +32,18 @@ function _formatSweepElapsed(startedAt) {
|
|
|
32
32
|
|
|
33
33
|
async function refreshKnowledgeBase() {
|
|
34
34
|
try {
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
_kbData
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
// Conditional fetch (browser-OOM fix): /api/knowledge is ~4.5MB and was
|
|
36
|
+
// re-fetched + re-parsed every 4s tick, the single biggest contributor to
|
|
37
|
+
// the heap churn that crashed the tab. On a 304 the payload is unchanged,
|
|
38
|
+
// so _kbData (which cross-slice readers like command-center.js depend on)
|
|
39
|
+
// is already current — skip the parse and the render entirely.
|
|
40
|
+
const resp = await _condFetchJson('/api/knowledge');
|
|
41
|
+
if (resp.notModified) return;
|
|
42
|
+
_kbData = resp.data;
|
|
43
|
+
// Second-level guard: an mtime-only ETag bust (e.g. a sweep touched a file)
|
|
44
|
+
// can arrive with byte-identical content — skip the full kb-list/kb-tabs
|
|
45
|
+
// rewrite when the payload matches last tick. Dedicated cache key
|
|
46
|
+
// 'kbPayload' avoids colliding with any other tracked slice (S1).
|
|
42
47
|
if (typeof _changed !== 'function' || _changed('kbPayload', _kbData)) {
|
|
43
48
|
renderKnowledgeBase();
|
|
44
49
|
}
|
package/dashboard.js
CHANGED
|
@@ -6425,6 +6425,33 @@ const server = http.createServer(async (req, res) => {
|
|
|
6425
6425
|
|
|
6426
6426
|
async function handleKnowledgeList(req, res) {
|
|
6427
6427
|
const entries = await getKnowledgeBaseEntries();
|
|
6428
|
+
// Content ETag (browser-OOM fix). /api/knowledge is ~4.5MB and the dashboard
|
|
6429
|
+
// polls it every 4s per foregrounded tab; without a 304 path the browser
|
|
6430
|
+
// re-downloaded + re-parsed the whole payload every tick, churning enough
|
|
6431
|
+
// heap to OOM-crash the tab. Derive the signature from the already-cached
|
|
6432
|
+
// entry metadata (count + per-file mtime/size) instead of stat-walking the
|
|
6433
|
+
// knowledge tree — that tree is ~9k files and the walk measured ~185ms,
|
|
6434
|
+
// which would block the event loop on every poll. getKnowledgeBaseEntries
|
|
6435
|
+
// is O(1) on a cache hit (30s TTL + explicit invalidation on KB writes and
|
|
6436
|
+
// sweeps), so this adds ~nothing on the hot path. Two small stats fold in
|
|
6437
|
+
// the sweep-badge files so the in-flight indicator still busts the tag.
|
|
6438
|
+
let h = 2166136261;
|
|
6439
|
+
for (const e of entries) {
|
|
6440
|
+
h = Math.imul(h ^ ((e.size || 0) >>> 0), 16777619);
|
|
6441
|
+
h = Math.imul(h ^ Math.floor(e.sortTs || 0), 16777619);
|
|
6442
|
+
}
|
|
6443
|
+
const sweepMtime = _maxInputMtimeMs([
|
|
6444
|
+
path.join(ENGINE_DIR, 'kb-swept.json'),
|
|
6445
|
+
path.join(ENGINE_DIR, 'kb-sweep-state.json'),
|
|
6446
|
+
]);
|
|
6447
|
+
const kbEtag = '"knowledge-' + entries.length + '-' + (h >>> 0).toString(36) + '-' + sweepMtime + '"';
|
|
6448
|
+
res.setHeader('ETag', kbEtag);
|
|
6449
|
+
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
|
|
6450
|
+
if (req && req.headers && req.headers['if-none-match'] === kbEtag) {
|
|
6451
|
+
res.statusCode = 304;
|
|
6452
|
+
res.end();
|
|
6453
|
+
return;
|
|
6454
|
+
}
|
|
6428
6455
|
const result = {};
|
|
6429
6456
|
for (const cat of shared.KB_CATEGORIES) result[cat] = [];
|
|
6430
6457
|
for (const e of entries) {
|
package/engine/lifecycle.js
CHANGED
|
@@ -546,6 +546,64 @@ function resolveWorkItemPath(meta) {
|
|
|
546
546
|
return null;
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
+
function getWorkItemPaths(config) {
|
|
550
|
+
const paths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
551
|
+
for (const p of shared.getProjects(config || {})) {
|
|
552
|
+
if (p && p.name) paths.push(shared.projectWorkItemsPath(p));
|
|
553
|
+
}
|
|
554
|
+
return paths;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function findWorkItemRecord(itemId, config) {
|
|
558
|
+
if (!itemId) return null;
|
|
559
|
+
for (const wiPath of getWorkItemPaths(config)) {
|
|
560
|
+
try {
|
|
561
|
+
const items = readOptionalJsonStrict(wiPath, 'work-items', Array.isArray) || [];
|
|
562
|
+
const item = items.find(i => i && i.id === itemId);
|
|
563
|
+
if (item) return { item, path: wiPath };
|
|
564
|
+
} catch { /* best-effort lineage lookup */ }
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function resolveWorkItemScheduleContext(item, config) {
|
|
570
|
+
if (!item || typeof item !== 'object') return {};
|
|
571
|
+
const seen = new Set();
|
|
572
|
+
let current = item;
|
|
573
|
+
let scheduleId = current._scheduleId || current._sourceScheduleId || '';
|
|
574
|
+
let scheduleWorkItemId = current._scheduleId ? current.id : (current._scheduleWorkItemId || '');
|
|
575
|
+
let projectName = current.project || '';
|
|
576
|
+
let parentItemId = current.parent_id || current.parentId || '';
|
|
577
|
+
|
|
578
|
+
while ((!scheduleId || !projectName) && parentItemId && !seen.has(parentItemId)) {
|
|
579
|
+
seen.add(parentItemId);
|
|
580
|
+
const found = findWorkItemRecord(parentItemId, config);
|
|
581
|
+
if (!found?.item) break;
|
|
582
|
+
current = found.item;
|
|
583
|
+
if (!scheduleId && (current._scheduleId || current._sourceScheduleId)) {
|
|
584
|
+
scheduleId = current._scheduleId || current._sourceScheduleId;
|
|
585
|
+
scheduleWorkItemId = current._scheduleId ? current.id : (current._scheduleWorkItemId || current.id);
|
|
586
|
+
}
|
|
587
|
+
if (!projectName && current.project) projectName = current.project;
|
|
588
|
+
parentItemId = current.parent_id || current.parentId || '';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
scheduleId: scheduleId || '',
|
|
593
|
+
scheduleWorkItemId: scheduleWorkItemId || '',
|
|
594
|
+
projectName: projectName || '',
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function applyScheduleContextToPrEntry(entry, item, config) {
|
|
599
|
+
const ctx = resolveWorkItemScheduleContext(item, config);
|
|
600
|
+
if (ctx.scheduleId) {
|
|
601
|
+
entry._sourceScheduleId = ctx.scheduleId;
|
|
602
|
+
if (ctx.scheduleWorkItemId) entry._scheduleWorkItemId = ctx.scheduleWorkItemId;
|
|
603
|
+
}
|
|
604
|
+
return entry;
|
|
605
|
+
}
|
|
606
|
+
|
|
549
607
|
/** Check if a work item is in a terminal completed state. */
|
|
550
608
|
function isItemCompleted(item) {
|
|
551
609
|
if (!item || typeof item !== 'object') return false;
|
|
@@ -920,6 +978,7 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
920
978
|
sourcePlan: meta?.item?.sourcePlan || '',
|
|
921
979
|
itemType: meta?.item?.itemType || ''
|
|
922
980
|
};
|
|
981
|
+
applyScheduleContextToPrEntry(entry, meta?.item, config);
|
|
923
982
|
// Issue #1772: one-off dispatches (e.g. human-initiated "review this PR" via CC)
|
|
924
983
|
// must not enroll the discovered PR into the auto eval loop. Tag _contextOnly so
|
|
925
984
|
// discoverFromPrs skips it for review/fix dispatch (still polled for status/comments).
|
|
@@ -1318,6 +1377,11 @@ function resolvePrFallbackProject(meta, config) {
|
|
|
1318
1377
|
const match = shared.resolveProjectSource(meta.item.project, projects, { allowCentral: false }).project;
|
|
1319
1378
|
if (match) return match;
|
|
1320
1379
|
}
|
|
1380
|
+
const scheduleContext = resolveWorkItemScheduleContext(meta?.item, config);
|
|
1381
|
+
if (scheduleContext.projectName) {
|
|
1382
|
+
const match = shared.resolveProjectSource(scheduleContext.projectName, projects, { allowCentral: false }).project;
|
|
1383
|
+
if (match) return match;
|
|
1384
|
+
}
|
|
1321
1385
|
return projects.length === 1 ? projects[0] : null;
|
|
1322
1386
|
}
|
|
1323
1387
|
|
|
@@ -1488,6 +1552,7 @@ function _attachFoundPrToWi(found, meta, agentId, resultSummary, config) {
|
|
|
1488
1552
|
sourcePlan: meta.item?.sourcePlan || '',
|
|
1489
1553
|
itemType: meta.item?.itemType || '',
|
|
1490
1554
|
};
|
|
1555
|
+
applyScheduleContextToPrEntry(entry, meta?.item, config);
|
|
1491
1556
|
shared.upsertPullRequestRecord(shared.projectPrPath(found.project), entry, {
|
|
1492
1557
|
project: found.project,
|
|
1493
1558
|
itemId: meta.item.id,
|
|
@@ -4184,6 +4249,9 @@ function processCompletionFollowups(completion, agentId, dispatchItem, config) {
|
|
|
4184
4249
|
}
|
|
4185
4250
|
if (raw.length === 0) return [];
|
|
4186
4251
|
const wiId = dispatchItem?.meta?.item?.id || 'N/A';
|
|
4252
|
+
const parentItem = dispatchItem?.meta?.item || null;
|
|
4253
|
+
const parentProject = dispatchItem?.meta?.project?.name || parentItem?.project || '';
|
|
4254
|
+
const parentSchedule = resolveWorkItemScheduleContext(parentItem, config);
|
|
4187
4255
|
let existingIds = null;
|
|
4188
4256
|
function loadExistingIds() {
|
|
4189
4257
|
if (existingIds !== null) return existingIds;
|
|
@@ -4217,6 +4285,27 @@ function processCompletionFollowups(completion, agentId, dispatchItem, config) {
|
|
|
4217
4285
|
if (allIds.size > 0 && !allIds.has(followupWiId)) {
|
|
4218
4286
|
log('warn', `Followup audit (${agentId || 'unknown'} wi=${wiId}): claimed followup ${followupWiId} not found in any work-items.json — dispatch may have been deduped, reverted, or never landed`);
|
|
4219
4287
|
}
|
|
4288
|
+
if (parentItem && (parentSchedule.scheduleId || parentProject)) {
|
|
4289
|
+
let stamped = false;
|
|
4290
|
+
for (const wiPath of getWorkItemPaths(config)) {
|
|
4291
|
+
mutateWorkItems(wiPath, items => {
|
|
4292
|
+
const target = Array.isArray(items) ? items.find(i => i && i.id === followupWiId) : null;
|
|
4293
|
+
if (!target) return items;
|
|
4294
|
+
if (!target.parent_id && target.id !== wiId) target.parent_id = wiId;
|
|
4295
|
+
if (!target.project && parentProject) target.project = parentProject;
|
|
4296
|
+
if (parentSchedule.scheduleId) {
|
|
4297
|
+
if (!target._sourceScheduleId) target._sourceScheduleId = parentSchedule.scheduleId;
|
|
4298
|
+
if (!target._scheduleWorkItemId) target._scheduleWorkItemId = parentSchedule.scheduleWorkItemId || wiId;
|
|
4299
|
+
}
|
|
4300
|
+
stamped = true;
|
|
4301
|
+
return items;
|
|
4302
|
+
});
|
|
4303
|
+
if (stamped) break;
|
|
4304
|
+
}
|
|
4305
|
+
if (stamped && parentSchedule.scheduleId) {
|
|
4306
|
+
log('info', `Followup audit (${agentId || 'unknown'} wi=${wiId}): linked ${followupWiId} to schedule ${parentSchedule.scheduleId}`);
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4220
4309
|
accepted.push({
|
|
4221
4310
|
wi_id: followupWiId,
|
|
4222
4311
|
title,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2153",
|
|
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"
|