@yemi33/minions 0.1.1658 → 0.1.1659
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 +5 -0
- package/dashboard.js +24 -37
- package/engine/cleanup.js +3 -3
- package/engine/cli.js +14 -11
- package/engine/cooldown.js +31 -21
- package/engine/copilot-models.json +1 -1
- package/engine/queries.js +15 -1
- package/engine/shared.js +22 -0
- package/engine.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -31,7 +31,7 @@ const dispatchMod = require('./engine/dispatch');
|
|
|
31
31
|
const steering = require('./engine/steering');
|
|
32
32
|
const os = require('os');
|
|
33
33
|
|
|
34
|
-
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, WORK_TYPE, reopenWorkItem } = shared;
|
|
34
|
+
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateControl, mutateCooldowns, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, WORK_TYPE, reopenWorkItem } = shared;
|
|
35
35
|
const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
|
|
36
36
|
getSkills, getInbox, getNotesWithMeta, getPullRequests,
|
|
37
37
|
getEngineLog, getMetrics, getKnowledgeBaseEntries, timeSince,
|
|
@@ -2116,12 +2116,10 @@ const { cleanDispatchEntries } = require('./engine/dispatch');
|
|
|
2116
2116
|
// ── Engine Restart Helpers (used by watchdog + API) ─────────────────────────
|
|
2117
2117
|
|
|
2118
2118
|
function spawnEngine() {
|
|
2119
|
-
const controlPath = path.join(ENGINE_DIR, 'control.json');
|
|
2120
2119
|
// Don't pre-write 'stopped' — let the new engine process own its state transition.
|
|
2121
2120
|
// The engine start code already handles state:'running' with a dead PID gracefully.
|
|
2122
2121
|
// Only set restarted_at + clear stale pid so dashboard shows the restart timestamp.
|
|
2123
|
-
|
|
2124
|
-
safeWrite(controlPath, { ...control, pid: null, restarted_at: new Date().toISOString() });
|
|
2122
|
+
mutateControl(control => ({ ...control, pid: null, restarted_at: new Date().toISOString() }));
|
|
2125
2123
|
const { spawn: cpSpawn } = require('child_process');
|
|
2126
2124
|
const childEnv = { ...process.env };
|
|
2127
2125
|
for (const key of Object.keys(childEnv)) {
|
|
@@ -2413,12 +2411,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
2413
2411
|
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
2414
2412
|
} catch (e) { console.error('dispatch cleanup:', e.message); }
|
|
2415
2413
|
try {
|
|
2416
|
-
|
|
2417
|
-
const cooldowns = safeJsonObj(cooldownPath);
|
|
2418
|
-
if (cooldowns[dispatchKey]) {
|
|
2414
|
+
mutateCooldowns(cooldowns => {
|
|
2419
2415
|
delete cooldowns[dispatchKey];
|
|
2420
|
-
|
|
2421
|
-
}
|
|
2416
|
+
return cooldowns;
|
|
2417
|
+
});
|
|
2422
2418
|
} catch (e) { console.error('cooldown cleanup:', e.message); }
|
|
2423
2419
|
|
|
2424
2420
|
return jsonReply(res, 200, { ok: true, id, rematerialized: true });
|
|
@@ -2463,12 +2459,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
2463
2459
|
|
|
2464
2460
|
// Clear cooldown so item isn't blocked by exponential backoff
|
|
2465
2461
|
try {
|
|
2466
|
-
|
|
2467
|
-
const cooldowns = safeJsonObj(cooldownPath);
|
|
2468
|
-
if (cooldowns[dispatchKey]) {
|
|
2462
|
+
mutateCooldowns(cooldowns => {
|
|
2469
2463
|
delete cooldowns[dispatchKey];
|
|
2470
|
-
|
|
2471
|
-
}
|
|
2464
|
+
return cooldowns;
|
|
2465
|
+
});
|
|
2472
2466
|
} catch (e) { console.error('cooldown cleanup:', e.message); }
|
|
2473
2467
|
|
|
2474
2468
|
return jsonReply(res, 200, { ok: true, id });
|
|
@@ -2514,13 +2508,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
2514
2508
|
|
|
2515
2509
|
// Clean cooldown entries so item can be re-created immediately
|
|
2516
2510
|
try {
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
}
|
|
2523
|
-
if (cleaned) safeWrite(cooldownPath, cooldowns);
|
|
2511
|
+
mutateCooldowns(cooldowns => {
|
|
2512
|
+
for (const key of Object.keys(cooldowns)) {
|
|
2513
|
+
if (key.includes(id)) delete cooldowns[key];
|
|
2514
|
+
}
|
|
2515
|
+
return cooldowns;
|
|
2516
|
+
});
|
|
2524
2517
|
} catch (e) { console.error('cooldown cleanup:', e.message); }
|
|
2525
2518
|
|
|
2526
2519
|
// Reset PRD item status so it doesn't stay 'dispatched' with no work item (#779)
|
|
@@ -2579,13 +2572,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
2579
2572
|
|
|
2580
2573
|
// Clean cooldown entries
|
|
2581
2574
|
try {
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
}
|
|
2588
|
-
if (cleaned) safeWrite(cooldownPath, cooldowns);
|
|
2575
|
+
mutateCooldowns(cooldowns => {
|
|
2576
|
+
for (const key of Object.keys(cooldowns)) {
|
|
2577
|
+
if (key.includes(id)) delete cooldowns[key];
|
|
2578
|
+
}
|
|
2579
|
+
return cooldowns;
|
|
2580
|
+
});
|
|
2589
2581
|
} catch (e) { console.error('cooldown cleanup on cancel:', e.message); }
|
|
2590
2582
|
|
|
2591
2583
|
invalidateStatusCache();
|
|
@@ -2704,12 +2696,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
2704
2696
|
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
2705
2697
|
} catch (e) { console.error('dispatch cleanup on reopen:', e.message); }
|
|
2706
2698
|
try {
|
|
2707
|
-
|
|
2708
|
-
const cooldowns = safeJsonObj(cooldownPath);
|
|
2709
|
-
if (cooldowns[dispatchKey]) {
|
|
2699
|
+
mutateCooldowns(cooldowns => {
|
|
2710
2700
|
delete cooldowns[dispatchKey];
|
|
2711
|
-
|
|
2712
|
-
}
|
|
2701
|
+
return cooldowns;
|
|
2702
|
+
});
|
|
2713
2703
|
} catch (e) { console.error('cooldown cleanup on reopen:', e.message); }
|
|
2714
2704
|
|
|
2715
2705
|
invalidateStatusCache();
|
|
@@ -6446,10 +6436,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6446
6436
|
|
|
6447
6437
|
// Engine
|
|
6448
6438
|
{ method: 'POST', path: '/api/engine/wakeup', desc: 'Trigger immediate engine tick via control.json signal', handler: async (req, res) => {
|
|
6449
|
-
|
|
6450
|
-
const control = shared.safeJson(controlPath) || {};
|
|
6451
|
-
control._wakeupAt = Date.now();
|
|
6452
|
-
shared.safeWrite(controlPath, control);
|
|
6439
|
+
shared.mutateControl(control => ({ ...control, _wakeupAt: Date.now() }));
|
|
6453
6440
|
return jsonReply(res, 200, { ok: true, message: 'Wakeup signal sent' });
|
|
6454
6441
|
}},
|
|
6455
6442
|
{ method: 'POST', path: '/api/engine/restart', desc: 'Force-kill engine and restart immediately', handler: handleEngineRestart },
|
package/engine/cleanup.js
CHANGED
|
@@ -9,7 +9,7 @@ const shared = require('./shared');
|
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
|
|
11
11
|
const { exec, execSilent, log, ts, ENGINE_DEFAULTS } = shared;
|
|
12
|
-
const { safeJson, safeWrite, safeReadDir, mutateWorkItems, mutateJsonFileLocked, getProjects, projectWorkItemsPath, projectPrPath,
|
|
12
|
+
const { safeJson, safeWrite, safeReadDir, mutateCooldowns, mutateWorkItems, mutateJsonFileLocked, getProjects, projectWorkItemsPath, projectPrPath,
|
|
13
13
|
sanitizeBranch, KB_CATEGORIES } = shared;
|
|
14
14
|
const { getDispatch, getAgentStatus } = queries;
|
|
15
15
|
|
|
@@ -675,9 +675,9 @@ function runCleanup(config, verbose = false) {
|
|
|
675
675
|
entries.sort((a, b) => (b[1].timestamp || 0) - (a[1].timestamp || 0));
|
|
676
676
|
const keep = Object.fromEntries(entries.slice(0, COOLDOWN_CAP));
|
|
677
677
|
cleaned.cooldowns = entries.length - COOLDOWN_CAP;
|
|
678
|
-
|
|
678
|
+
mutateCooldowns(() => keep);
|
|
679
679
|
} else if (dirty) {
|
|
680
|
-
|
|
680
|
+
mutateCooldowns(() => cooldowns);
|
|
681
681
|
}
|
|
682
682
|
}
|
|
683
683
|
} catch (e) { log('warn', 'cap cooldowns: ' + e.message); }
|
package/engine/cli.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const shared = require('./shared');
|
|
9
|
-
const { safeRead, safeJson, safeWrite, mutateWorkItems, ts, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
9
|
+
const { safeRead, safeJson, safeWrite, mutateControl, mutateWorkItems, ts, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
10
10
|
const queries = require('./queries');
|
|
11
11
|
const { getConfig, getControl, getDispatch, getAgentStatus,
|
|
12
12
|
MINIONS_DIR, ENGINE_DIR, AGENTS_DIR, PLANS_DIR, PRD_DIR, CONTROL_PATH, DISPATCH_PATH } = queries;
|
|
@@ -312,7 +312,7 @@ const commands = {
|
|
|
312
312
|
}
|
|
313
313
|
let codeCommit = null;
|
|
314
314
|
try { codeCommit = require('child_process').execSync('git rev-parse --short HEAD', { cwd: path.resolve(__dirname, '..'), encoding: 'utf8', timeout: 5000, windowsHide: true }).trim(); } catch {}
|
|
315
|
-
|
|
315
|
+
mutateControl(() => ({ state: 'running', pid: process.pid, started_at: e.ts(), codeVersion, codeCommit }));
|
|
316
316
|
// Keep .minions-version in sync so `minions version` stays accurate after git pulls
|
|
317
317
|
if (codeVersion) {
|
|
318
318
|
try { fs.writeFileSync(path.join(shared.MINIONS_DIR, '.minions-version'), codeVersion); } catch {}
|
|
@@ -599,7 +599,10 @@ const commands = {
|
|
|
599
599
|
const ctrl = getControl();
|
|
600
600
|
if (ctrl._wakeupAt && Date.now() - ctrl._wakeupAt < 5000) {
|
|
601
601
|
delete ctrl._wakeupAt;
|
|
602
|
-
|
|
602
|
+
mutateControl((control) => {
|
|
603
|
+
delete control._wakeupAt;
|
|
604
|
+
return control;
|
|
605
|
+
});
|
|
603
606
|
e.tick();
|
|
604
607
|
}
|
|
605
608
|
}, 1000);
|
|
@@ -669,11 +672,11 @@ const commands = {
|
|
|
669
672
|
clearInterval(fastPollTimer);
|
|
670
673
|
if (teamsInboxTimer) clearInterval(teamsInboxTimer);
|
|
671
674
|
for (const f of _watchedFiles) { try { fs.unwatchFile(f); } catch { /* cleanup */ } }
|
|
672
|
-
|
|
675
|
+
mutateControl(() => ({ state: 'stopping', pid: process.pid, stopping_at: e.ts() }));
|
|
673
676
|
e.log('info', `Graceful shutdown initiated (${signal})`);
|
|
674
677
|
|
|
675
678
|
if (e.activeProcesses.size === 0) {
|
|
676
|
-
|
|
679
|
+
mutateControl(() => ({ state: 'stopped', stopped_at: e.ts() }));
|
|
677
680
|
e.log('info', 'Graceful shutdown complete (no active agents)');
|
|
678
681
|
shared.flushLogs(); // drain buffered log entries before exit
|
|
679
682
|
console.log('No active agents — stopped.');
|
|
@@ -687,7 +690,7 @@ const commands = {
|
|
|
687
690
|
const poll = setInterval(() => {
|
|
688
691
|
if (e.activeProcesses.size === 0) {
|
|
689
692
|
clearInterval(poll);
|
|
690
|
-
|
|
693
|
+
mutateControl(() => ({ state: 'stopped', stopped_at: e.ts() }));
|
|
691
694
|
e.log('info', 'Graceful shutdown complete (all agents finished)');
|
|
692
695
|
shared.flushLogs(); // drain buffered log entries before exit
|
|
693
696
|
console.log('All agents finished — stopped.');
|
|
@@ -695,7 +698,7 @@ const commands = {
|
|
|
695
698
|
}
|
|
696
699
|
if (Date.now() >= deadline) {
|
|
697
700
|
clearInterval(poll);
|
|
698
|
-
|
|
701
|
+
mutateControl(() => ({ state: 'stopped', stopped_at: e.ts() }));
|
|
699
702
|
e.log('warn', `Graceful shutdown timed out after ${timeout / 1000}s with ${e.activeProcesses.size} agent(s) still active`);
|
|
700
703
|
shared.flushLogs(); // drain buffered log entries before exit
|
|
701
704
|
console.log(`Shutdown timeout (${timeout / 1000}s) — force exiting with ${e.activeProcesses.size} agent(s) still running.`);
|
|
@@ -742,14 +745,14 @@ const commands = {
|
|
|
742
745
|
if (control.pid && control.pid !== process.pid) {
|
|
743
746
|
try { process.kill(control.pid); } catch { /* process may be dead */ }
|
|
744
747
|
}
|
|
745
|
-
|
|
748
|
+
mutateControl(() => ({ state: 'stopped', stopped_at: e.ts() }));
|
|
746
749
|
e.log('info', 'Engine stopped');
|
|
747
750
|
console.log('Engine stopped.');
|
|
748
751
|
},
|
|
749
752
|
|
|
750
753
|
pause() {
|
|
751
754
|
const e = engine();
|
|
752
|
-
|
|
755
|
+
mutateControl(() => ({ state: 'paused', paused_at: e.ts() }));
|
|
753
756
|
e.log('info', 'Engine paused');
|
|
754
757
|
console.log('Engine paused. Run `node .minions/engine.js resume` to resume.');
|
|
755
758
|
},
|
|
@@ -761,7 +764,7 @@ const commands = {
|
|
|
761
764
|
console.log('Engine is already running.');
|
|
762
765
|
return;
|
|
763
766
|
}
|
|
764
|
-
|
|
767
|
+
mutateControl(() => ({ state: 'running', resumed_at: e.ts() }));
|
|
765
768
|
e.log('info', 'Engine resumed');
|
|
766
769
|
console.log('Engine resumed.');
|
|
767
770
|
},
|
|
@@ -932,7 +935,7 @@ const commands = {
|
|
|
932
935
|
dispatch() {
|
|
933
936
|
const control = getControl();
|
|
934
937
|
if (control.state === 'running' && isEngineProcessAlive(control)) {
|
|
935
|
-
|
|
938
|
+
mutateControl((c) => ({ ...c, _wakeupAt: Date.now() }));
|
|
936
939
|
console.log(`Dispatch wakeup requested from running engine (PID ${control.pid}).`);
|
|
937
940
|
return;
|
|
938
941
|
}
|
package/engine/cooldown.js
CHANGED
|
@@ -7,7 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
8
|
const queries = require('./queries');
|
|
9
9
|
|
|
10
|
-
const { safeJson,
|
|
10
|
+
const { safeJson, mutateCooldowns, log, ENGINE_DEFAULTS } = shared;
|
|
11
11
|
const { ENGINE_DIR } = queries;
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -37,6 +37,7 @@ function _truncateContextEntry(entry, maxBytes) {
|
|
|
37
37
|
|
|
38
38
|
const COOLDOWN_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
39
39
|
const dispatchCooldowns = new Map(); // key → { timestamp, failures }
|
|
40
|
+
let _lastDiskCooldownKeys = new Set();
|
|
40
41
|
|
|
41
42
|
function loadCooldowns() {
|
|
42
43
|
const saved = safeJson(COOLDOWN_PATH);
|
|
@@ -48,6 +49,7 @@ function loadCooldowns() {
|
|
|
48
49
|
dispatchCooldowns.set(k, v);
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
_lastDiskCooldownKeys = new Set(dispatchCooldowns.keys());
|
|
51
53
|
log('info', `Loaded ${dispatchCooldowns.size} cooldowns from disk`);
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -57,27 +59,35 @@ function saveCooldowns() {
|
|
|
57
59
|
if (_cooldownWriteTimer) clearTimeout(_cooldownWriteTimer);
|
|
58
60
|
_cooldownWriteTimer = setTimeout(() => {
|
|
59
61
|
_cooldownWriteTimer = null;
|
|
60
|
-
// Prune expired entries (>24h) before saving
|
|
61
|
-
const now = Date.now();
|
|
62
|
-
for (const [k, v] of dispatchCooldowns) {
|
|
63
|
-
if (now - v.timestamp > 24 * 60 * 60 * 1000) dispatchCooldowns.delete(k);
|
|
64
|
-
}
|
|
65
|
-
// Trim pendingContexts arrays before writing to prevent bloat
|
|
66
|
-
const cap = ENGINE_DEFAULTS.maxPendingContexts;
|
|
67
|
-
const entryLimit = ENGINE_DEFAULTS.maxPendingContextEntryBytes;
|
|
68
|
-
for (const [, v] of dispatchCooldowns) {
|
|
69
|
-
if (Array.isArray(v.pendingContexts)) {
|
|
70
|
-
if (v.pendingContexts.length > cap) {
|
|
71
|
-
v.pendingContexts = v.pendingContexts.slice(-cap);
|
|
72
|
-
}
|
|
73
|
-
// Also truncate oversized individual entries — #1167 showed
|
|
74
|
-
// 20 entries × 25 MB each still produced a 500 MB cooldowns.json.
|
|
75
|
-
v.pendingContexts = v.pendingContexts.map(e => _truncateContextEntry(e, entryLimit));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const obj = Object.fromEntries(dispatchCooldowns);
|
|
79
62
|
try {
|
|
80
|
-
|
|
63
|
+
mutateCooldowns((diskCooldowns) => {
|
|
64
|
+
for (const key of Array.from(dispatchCooldowns.keys())) {
|
|
65
|
+
if (_lastDiskCooldownKeys.has(key) && !Object.prototype.hasOwnProperty.call(diskCooldowns, key)) {
|
|
66
|
+
dispatchCooldowns.delete(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Prune expired entries (>24h) before saving
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
for (const [k, v] of dispatchCooldowns) {
|
|
72
|
+
if (now - v.timestamp > 24 * 60 * 60 * 1000) dispatchCooldowns.delete(k);
|
|
73
|
+
}
|
|
74
|
+
// Trim pendingContexts arrays before writing to prevent bloat
|
|
75
|
+
const cap = ENGINE_DEFAULTS.maxPendingContexts;
|
|
76
|
+
const entryLimit = ENGINE_DEFAULTS.maxPendingContextEntryBytes;
|
|
77
|
+
for (const [, v] of dispatchCooldowns) {
|
|
78
|
+
if (Array.isArray(v.pendingContexts)) {
|
|
79
|
+
if (v.pendingContexts.length > cap) {
|
|
80
|
+
v.pendingContexts = v.pendingContexts.slice(-cap);
|
|
81
|
+
}
|
|
82
|
+
// Also truncate oversized individual entries — #1167 showed
|
|
83
|
+
// 20 entries × 25 MB each still produced a 500 MB cooldowns.json.
|
|
84
|
+
v.pendingContexts = v.pendingContexts.map(e => _truncateContextEntry(e, entryLimit));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const obj = Object.fromEntries(dispatchCooldowns);
|
|
88
|
+
_lastDiskCooldownKeys = new Set(Object.keys(obj));
|
|
89
|
+
return obj;
|
|
90
|
+
});
|
|
81
91
|
} catch (err) {
|
|
82
92
|
log('warn', `saveCooldowns failed writing ${COOLDOWN_PATH}: ${err.message}`);
|
|
83
93
|
}
|
package/engine/queries.js
CHANGED
|
@@ -101,7 +101,21 @@ function timeSince(ms) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function readJsonNoRestore(filePath) {
|
|
104
|
-
|
|
104
|
+
let raw;
|
|
105
|
+
try {
|
|
106
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (e && e.code !== 'ENOENT') {
|
|
109
|
+
console.warn(`[queries] failed to read ${_relativeStatePath(filePath)}: ${e.message}`);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(raw);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.warn(`[queries] corrupt JSON in ${_relativeStatePath(filePath)}: ${e.message}`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
// ── Core State Readers ──────────────────────────────────────────────────────
|
package/engine/shared.js
CHANGED
|
@@ -8,6 +8,9 @@ const path = require('path');
|
|
|
8
8
|
const crypto = require('crypto');
|
|
9
9
|
|
|
10
10
|
const MINIONS_DIR = process.env.MINIONS_TEST_DIR || path.resolve(__dirname, '..');
|
|
11
|
+
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
12
|
+
const CONTROL_PATH = path.join(ENGINE_DIR, 'control.json');
|
|
13
|
+
const COOLDOWNS_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
11
14
|
const PR_LINKS_PATH = path.join(MINIONS_DIR, 'engine', 'pr-links.json');
|
|
12
15
|
const PINNED_ITEMS_PATH = path.join(MINIONS_DIR, 'engine', 'kb-pins.json');
|
|
13
16
|
const LOG_PATH = path.join(MINIONS_DIR, 'engine', 'log.json');
|
|
@@ -428,6 +431,20 @@ function mutateJsonFileLocked(filePath, mutateFn, {
|
|
|
428
431
|
}, { retries, retryBackoffMs });
|
|
429
432
|
}
|
|
430
433
|
|
|
434
|
+
function mutateControl(mutator) {
|
|
435
|
+
return mutateJsonFileLocked(CONTROL_PATH, (data) => {
|
|
436
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
437
|
+
return mutator(data) || data;
|
|
438
|
+
}, { defaultValue: { state: 'stopped', pid: null }, skipWriteIfUnchanged: true });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function mutateCooldowns(mutator) {
|
|
442
|
+
return mutateJsonFileLocked(COOLDOWNS_PATH, (data) => {
|
|
443
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
444
|
+
return mutator(data) || data;
|
|
445
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
446
|
+
}
|
|
447
|
+
|
|
431
448
|
/**
|
|
432
449
|
* Generate a unique ID suffix: timestamp + 4 random chars.
|
|
433
450
|
* Use for filenames that could collide (dispatch IDs, temp files, etc.)
|
|
@@ -2304,6 +2321,9 @@ function createThrottleTracker({ label, baseBackoffMs = 60000, maxBackoffMs = 32
|
|
|
2304
2321
|
|
|
2305
2322
|
module.exports = {
|
|
2306
2323
|
MINIONS_DIR,
|
|
2324
|
+
ENGINE_DIR,
|
|
2325
|
+
CONTROL_PATH,
|
|
2326
|
+
COOLDOWNS_PATH,
|
|
2307
2327
|
PR_LINKS_PATH,
|
|
2308
2328
|
PINNED_ITEMS_PATH,
|
|
2309
2329
|
LOG_PATH,
|
|
@@ -2325,6 +2345,8 @@ module.exports = {
|
|
|
2325
2345
|
assertStateFileSize,
|
|
2326
2346
|
withFileLock,
|
|
2327
2347
|
mutateJsonFileLocked,
|
|
2348
|
+
mutateControl,
|
|
2349
|
+
mutateCooldowns,
|
|
2328
2350
|
mutateWorkItems,
|
|
2329
2351
|
reopenWorkItem,
|
|
2330
2352
|
mutatePullRequests,
|
package/engine.js
CHANGED
|
@@ -95,6 +95,7 @@ const safeRead = shared.safeRead;
|
|
|
95
95
|
const safeWrite = shared.safeWrite;
|
|
96
96
|
const safeUnlink = shared.safeUnlink;
|
|
97
97
|
const mutateJsonFileLocked = shared.mutateJsonFileLocked;
|
|
98
|
+
const mutateControl = shared.mutateControl;
|
|
98
99
|
const mutateWorkItems = shared.mutateWorkItems;
|
|
99
100
|
const mutatePullRequests = shared.mutatePullRequests;
|
|
100
101
|
const withFileLock = shared.withFileLock;
|
|
@@ -3557,7 +3558,7 @@ async function tickInner() {
|
|
|
3557
3558
|
}
|
|
3558
3559
|
|
|
3559
3560
|
// Write heartbeat so dashboard can detect stale engine
|
|
3560
|
-
try {
|
|
3561
|
+
try { mutateControl(c => ({ ...c, heartbeat: Date.now() })); } catch (e) { log('warn', 'write heartbeat: ' + e.message); }
|
|
3561
3562
|
|
|
3562
3563
|
const config = getConfig();
|
|
3563
3564
|
tickCount++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1659",
|
|
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"
|