@yemi33/minions 0.1.475 → 0.1.477
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 -1
- package/engine/queries.js +111 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.477 (2026-04-07)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- Add mtime-based caching to getPrdInfo()
|
|
7
|
+
- Optimize getAgentStatus() to read only head+tail of live-output.log
|
|
6
8
|
- Fix unlocked metrics.json in lifecycle.js post-merge hook
|
|
7
9
|
- Fix unlocked metrics.json read-modify-write in trackEngineUsage
|
|
8
10
|
- Limit SSE initial live-stream payload to last 64KB
|
package/engine/queries.js
CHANGED
|
@@ -13,6 +13,35 @@ const { safeRead, safeReadDir, safeJson, safeWrite, getProjects,
|
|
|
13
13
|
projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
|
|
14
14
|
WI_STATUS } = shared;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Read the first `bytes` and last `bytes` of a file efficiently using byte offsets.
|
|
18
|
+
* For files <= 2*bytes, reads the whole file. Returns { head, tail } strings.
|
|
19
|
+
* Returns { head: '', tail: '' } on any error.
|
|
20
|
+
*/
|
|
21
|
+
function readHeadTail(filePath, bytes = 1024) {
|
|
22
|
+
try {
|
|
23
|
+
const stat = fs.statSync(filePath);
|
|
24
|
+
const size = stat.size;
|
|
25
|
+
if (size === 0) return { head: '', tail: '' };
|
|
26
|
+
if (size <= bytes * 2) {
|
|
27
|
+
const full = fs.readFileSync(filePath, 'utf8');
|
|
28
|
+
return { head: full, tail: full };
|
|
29
|
+
}
|
|
30
|
+
const fd = fs.openSync(filePath, 'r');
|
|
31
|
+
try {
|
|
32
|
+
const headBuf = Buffer.alloc(bytes);
|
|
33
|
+
fs.readSync(fd, headBuf, 0, bytes, 0);
|
|
34
|
+
const tailBuf = Buffer.alloc(bytes);
|
|
35
|
+
fs.readSync(fd, tailBuf, 0, bytes, size - bytes);
|
|
36
|
+
return { head: headBuf.toString('utf8'), tail: tailBuf.toString('utf8') };
|
|
37
|
+
} finally {
|
|
38
|
+
fs.closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
return { head: '', tail: '' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
16
45
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
17
46
|
|
|
18
47
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
@@ -130,19 +159,20 @@ function getAgentStatus(agentId) {
|
|
|
130
159
|
branch: active.meta?.branch || '',
|
|
131
160
|
started_at: active.started_at || active.created_at || null,
|
|
132
161
|
};
|
|
133
|
-
// Detect permission-waiting:
|
|
162
|
+
// Detect permission-waiting: read only head+tail of live-output.log (max 2KB total)
|
|
134
163
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
164
|
+
const liveLogPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
165
|
+
const { head, tail } = readHeadTail(liveLogPath, 1024);
|
|
166
|
+
if (head) {
|
|
167
|
+
// Check init message (in head) for permission mode
|
|
168
|
+
const initMatch = head.match(/"permissionMode"\s*:\s*"([^"]+)"/);
|
|
139
169
|
if (initMatch && initMatch[1] !== 'bypassPermissions') {
|
|
140
170
|
result._permissionMode = initMatch[1];
|
|
141
171
|
}
|
|
142
|
-
// Check if agent has been silent for >60s (
|
|
143
|
-
const lastLine =
|
|
172
|
+
// Check if agent has been silent for >60s (use tail for recent activity)
|
|
173
|
+
const lastLine = tail.trimEnd().split('\n').pop();
|
|
144
174
|
if (lastLine && lastLine.includes('"type":"assistant"') && lastLine.includes('"tool_use"')) {
|
|
145
|
-
const liveStat = fs.statSync(
|
|
175
|
+
const liveStat = fs.statSync(liveLogPath);
|
|
146
176
|
const silentMs = Date.now() - liveStat.mtimeMs;
|
|
147
177
|
if (silentMs > 60000 && result._permissionMode) {
|
|
148
178
|
result._warning = 'Possibly waiting for permission approval — agent is not in bypass mode';
|
|
@@ -597,12 +627,51 @@ function getWorkItems(config) {
|
|
|
597
627
|
|
|
598
628
|
// ── PRD Progress ────────────────────────────────────────────────────────────
|
|
599
629
|
|
|
630
|
+
// Module-level caches for getPrdInfo() — avoids re-reading unchanged PRD files
|
|
631
|
+
const _prdFileCache = new Map(); // filePath → { mtimeMs, plan }
|
|
632
|
+
let _prdDirMtimes = { prd: 0, archive: 0 }; // directory mtimes to detect new/deleted files
|
|
633
|
+
let _prdResultCache = null; // cached final result
|
|
634
|
+
let _prdResultInputHash = ''; // hash of all input mtimes to detect any change
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Collect mtimes of all input files that affect getPrdInfo() output.
|
|
638
|
+
* Returns a string hash for quick equality check plus the dir mtimes.
|
|
639
|
+
*/
|
|
640
|
+
function _getPrdInputHash(projects) {
|
|
641
|
+
const mtimes = [];
|
|
642
|
+
// PRD directory mtimes (detect new/deleted files)
|
|
643
|
+
let prdDirMtime = 0, archiveDirMtime = 0;
|
|
644
|
+
try { prdDirMtime = fs.statSync(PRD_DIR).mtimeMs; } catch { /* optional */ }
|
|
645
|
+
const archiveDir = path.join(PRD_DIR, 'archive');
|
|
646
|
+
try { archiveDirMtime = fs.statSync(archiveDir).mtimeMs; } catch { /* optional */ }
|
|
647
|
+
mtimes.push(prdDirMtime, archiveDirMtime);
|
|
648
|
+
// Work-items file mtimes (affect status display)
|
|
649
|
+
for (const project of projects) {
|
|
650
|
+
try { mtimes.push(fs.statSync(projectWorkItemsPath(project)).mtimeMs); } catch { mtimes.push(0); }
|
|
651
|
+
}
|
|
652
|
+
try { mtimes.push(fs.statSync(path.join(MINIONS_DIR, 'work-items.json')).mtimeMs); } catch { mtimes.push(0); }
|
|
653
|
+
// PR file mtimes (affect PR links)
|
|
654
|
+
for (const project of projects) {
|
|
655
|
+
try { mtimes.push(fs.statSync(projectPrPath(project)).mtimeMs); } catch { mtimes.push(0); }
|
|
656
|
+
}
|
|
657
|
+
return { hash: mtimes.join(','), prdDirMtime, archiveDirMtime };
|
|
658
|
+
}
|
|
659
|
+
|
|
600
660
|
function getPrdInfo(config) {
|
|
601
661
|
config = config || getConfig();
|
|
602
662
|
const projects = getProjects(config);
|
|
663
|
+
|
|
664
|
+
// Quick mtime check — return cached result if nothing changed
|
|
665
|
+
const { hash, prdDirMtime, archiveDirMtime } = _getPrdInputHash(projects);
|
|
666
|
+
if (_prdResultCache && hash === _prdResultInputHash) return _prdResultCache;
|
|
667
|
+
|
|
603
668
|
let allPrdItems = [];
|
|
604
669
|
let latestStat = null;
|
|
605
670
|
|
|
671
|
+
// Check if directory listings need refresh
|
|
672
|
+
const dirsChanged = prdDirMtime !== _prdDirMtimes.prd || archiveDirMtime !== _prdDirMtimes.archive;
|
|
673
|
+
_prdDirMtimes = { prd: prdDirMtime, archive: archiveDirMtime };
|
|
674
|
+
|
|
606
675
|
// Scan active PRDs and archived PRDs (completed PRDs still need to show progress)
|
|
607
676
|
const planDirs = [
|
|
608
677
|
{ dir: PRD_DIR, archived: false },
|
|
@@ -613,10 +682,21 @@ function getPrdInfo(config) {
|
|
|
613
682
|
const planFiles = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
614
683
|
for (const pf of planFiles) {
|
|
615
684
|
try {
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
const stat = fs.statSync(path.join(dir, pf));
|
|
685
|
+
const filePath = path.join(dir, pf);
|
|
686
|
+
const stat = fs.statSync(filePath);
|
|
619
687
|
if (!latestStat || stat.mtimeMs > latestStat.mtimeMs) latestStat = stat;
|
|
688
|
+
|
|
689
|
+
// Per-file mtime cache: only re-read files that changed
|
|
690
|
+
const cached = _prdFileCache.get(filePath);
|
|
691
|
+
let plan;
|
|
692
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
693
|
+
plan = cached.plan;
|
|
694
|
+
} else {
|
|
695
|
+
plan = safeJson(filePath);
|
|
696
|
+
_prdFileCache.set(filePath, { mtimeMs: stat.mtimeMs, plan });
|
|
697
|
+
}
|
|
698
|
+
if (!plan || !plan.missing_features) continue;
|
|
699
|
+
|
|
620
700
|
// Staleness: compare source plan mtime to recorded sourcePlanModifiedAt
|
|
621
701
|
let planStale = false;
|
|
622
702
|
if (!archived && plan.source_plan) {
|
|
@@ -638,6 +718,12 @@ function getPrdInfo(config) {
|
|
|
638
718
|
}
|
|
639
719
|
} catch { /* optional */ }
|
|
640
720
|
}
|
|
721
|
+
// Clean stale entries from file cache when dirs changed
|
|
722
|
+
if (dirsChanged) {
|
|
723
|
+
for (const cachedPath of _prdFileCache.keys()) {
|
|
724
|
+
if (cachedPath.startsWith(dir) && !fs.existsSync(cachedPath)) _prdFileCache.delete(cachedPath);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
641
727
|
} catch { /* optional */ }
|
|
642
728
|
}
|
|
643
729
|
|
|
@@ -736,7 +822,18 @@ function getPrdInfo(config) {
|
|
|
736
822
|
missingList: items.filter(i => i.status === 'missing').map(f => ({ id: f.id, name: f.name || f.title, priority: f.priority, complexity: f.estimated_complexity || f.size })),
|
|
737
823
|
};
|
|
738
824
|
|
|
739
|
-
|
|
825
|
+
const result = { progress, status };
|
|
826
|
+
_prdResultCache = result;
|
|
827
|
+
_prdResultInputHash = hash;
|
|
828
|
+
return result;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** Reset PRD info cache — exported for testing */
|
|
832
|
+
function resetPrdInfoCache() {
|
|
833
|
+
_prdFileCache.clear();
|
|
834
|
+
_prdDirMtimes = { prd: 0, archive: 0 };
|
|
835
|
+
_prdResultCache = null;
|
|
836
|
+
_prdResultInputHash = '';
|
|
740
837
|
}
|
|
741
838
|
|
|
742
839
|
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
@@ -748,6 +845,8 @@ module.exports = {
|
|
|
748
845
|
|
|
749
846
|
// Helpers
|
|
750
847
|
timeSince,
|
|
848
|
+
readHeadTail, // exported for testing
|
|
849
|
+
resetPrdInfoCache,
|
|
751
850
|
|
|
752
851
|
// Core state
|
|
753
852
|
getConfig, getControl, getDispatch, getDispatchQueue,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.477",
|
|
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"
|