@yemi33/minions 0.1.1034 → 0.1.1036
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 +81 -0
- package/dashboard.js +18 -0
- package/engine/cli.js +12 -0
- package/engine/cooldown.js +39 -3
- package/engine/dispatch.js +28 -1
- package/engine/shared.js +121 -0
- package/engine.js +5 -1
- package/package.json +1 -1
- package/playbooks/test.md +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1036 (2026-04-16)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- Doc-chat performance — debounced persistence, session TTL, smart disk reads (#1164)
|
|
7
|
+
- split getStatus() into fast/slow state tiers with separate TTLs (#1170)
|
|
8
|
+
- Cache _countWorktrees() with 30s TTL (#1166)
|
|
9
|
+
- Pre-cache gzipped status buffer alongside JSON cache (#1171)
|
|
10
|
+
- parallelize ADO and GitHub PR polling with Promise.allSettled (#1172)
|
|
11
|
+
- route implement items to dedicated implement playbook (#1115)
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
- allow test tasks to create PRs when files are modified
|
|
15
|
+
- sidecar oversized dispatch prompts to prevent dashboard OOM (closes #1167) (#1183)
|
|
16
|
+
- use locked write for failed-with-PR reconciliation in cleanup.js (#1169)
|
|
17
|
+
- prevent test from corrupting live meeting state
|
|
18
|
+
- scheduler enabled field falsy check treats undefined as disabled (#1160)
|
|
19
|
+
- update branch-dedup tests to use canonical PR IDs after merge with master (#1146)
|
|
20
|
+
- auto-review not firing for manually-linked PRs with autoObserve=true
|
|
21
|
+
- mark PR abandoned on 404 instead of silently retrying each tick
|
|
22
|
+
- replace undefined PROJECTS with config.projects in checkWatches (#1108)
|
|
23
|
+
- write permission for publish workflow
|
|
24
|
+
- run tests inline and post check runs for publish PRs
|
|
25
|
+
- add maxBuffer to all GitHub CLI spawns + paginate PR list (#1130)
|
|
26
|
+
- add required CI checks for PRs + update publish for auto-merge
|
|
27
|
+
- revert to PR merge now that stale status check is removed
|
|
28
|
+
- guard live review check against undefined vote/state values (#1132)
|
|
29
|
+
- push version bump directly to master instead of via PR
|
|
30
|
+
- add push-triggered CI for chore/publish branches
|
|
31
|
+
- publish workflow chore PRs failing to merge
|
|
32
|
+
- harden KB ordering
|
|
33
|
+
- harden audited state transitions
|
|
34
|
+
|
|
35
|
+
### Other
|
|
36
|
+
- [E2E] Dashboard & engine perf: gzip cache, tiered status, lock backoff, polling parallelization (#1174)
|
|
37
|
+
- Fix doc chat session isolation
|
|
38
|
+
- test(pipeline): add unit tests for CRUD, stage execution, run lifecycle (#1162)
|
|
39
|
+
- chore: test publish after removing stale status check
|
|
40
|
+
- chore: trigger publish test
|
|
41
|
+
- chore: test publish workflow fix
|
|
42
|
+
- chore: trigger publish workflow test
|
|
43
|
+
|
|
44
|
+
## 0.1.1035 (2026-04-16)
|
|
45
|
+
|
|
46
|
+
### Features
|
|
47
|
+
- Doc-chat performance — debounced persistence, session TTL, smart disk reads (#1164)
|
|
48
|
+
- split getStatus() into fast/slow state tiers with separate TTLs (#1170)
|
|
49
|
+
- Cache _countWorktrees() with 30s TTL (#1166)
|
|
50
|
+
- Pre-cache gzipped status buffer alongside JSON cache (#1171)
|
|
51
|
+
- parallelize ADO and GitHub PR polling with Promise.allSettled (#1172)
|
|
52
|
+
- route implement items to dedicated implement playbook (#1115)
|
|
53
|
+
|
|
54
|
+
### Fixes
|
|
55
|
+
- sidecar oversized dispatch prompts to prevent dashboard OOM (closes #1167) (#1183)
|
|
56
|
+
- use locked write for failed-with-PR reconciliation in cleanup.js (#1169)
|
|
57
|
+
- prevent test from corrupting live meeting state
|
|
58
|
+
- scheduler enabled field falsy check treats undefined as disabled (#1160)
|
|
59
|
+
- update branch-dedup tests to use canonical PR IDs after merge with master (#1146)
|
|
60
|
+
- auto-review not firing for manually-linked PRs with autoObserve=true
|
|
61
|
+
- mark PR abandoned on 404 instead of silently retrying each tick
|
|
62
|
+
- replace undefined PROJECTS with config.projects in checkWatches (#1108)
|
|
63
|
+
- write permission for publish workflow
|
|
64
|
+
- run tests inline and post check runs for publish PRs
|
|
65
|
+
- add maxBuffer to all GitHub CLI spawns + paginate PR list (#1130)
|
|
66
|
+
- add required CI checks for PRs + update publish for auto-merge
|
|
67
|
+
- revert to PR merge now that stale status check is removed
|
|
68
|
+
- guard live review check against undefined vote/state values (#1132)
|
|
69
|
+
- push version bump directly to master instead of via PR
|
|
70
|
+
- add push-triggered CI for chore/publish branches
|
|
71
|
+
- publish workflow chore PRs failing to merge
|
|
72
|
+
- harden KB ordering
|
|
73
|
+
- harden audited state transitions
|
|
74
|
+
|
|
75
|
+
### Other
|
|
76
|
+
- [E2E] Dashboard & engine perf: gzip cache, tiered status, lock backoff, polling parallelization (#1174)
|
|
77
|
+
- Fix doc chat session isolation
|
|
78
|
+
- test(pipeline): add unit tests for CRUD, stage execution, run lifecycle (#1162)
|
|
79
|
+
- chore: test publish after removing stale status check
|
|
80
|
+
- chore: trigger publish test
|
|
81
|
+
- chore: test publish workflow fix
|
|
82
|
+
- chore: trigger publish workflow test
|
|
83
|
+
|
|
3
84
|
## 0.1.1034 (2026-04-16)
|
|
4
85
|
|
|
5
86
|
### Features
|
package/dashboard.js
CHANGED
|
@@ -32,6 +32,24 @@ const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
|
|
|
32
32
|
getEngineLog, getMetrics, getKnowledgeBaseEntries, timeSince,
|
|
33
33
|
MINIONS_DIR, AGENTS_DIR, ENGINE_DIR, INBOX_DIR, DISPATCH_PATH, PRD_DIR } = queries;
|
|
34
34
|
|
|
35
|
+
// Startup size guard (#1167): fail fast with a clear error when dispatch.json /
|
|
36
|
+
// cooldowns.json have ballooned past ENGINE_DEFAULTS.maxStateFileBytes. Without
|
|
37
|
+
// this, V8 silently OOMs on JSON.parse(~1 GB) and the operator has no hint as to
|
|
38
|
+
// which file is bloated. The thrown error names the file and directs to
|
|
39
|
+
// engine/contexts/ where sidecars live.
|
|
40
|
+
(() => {
|
|
41
|
+
const stateFiles = [
|
|
42
|
+
DISPATCH_PATH,
|
|
43
|
+
path.join(ENGINE_DIR, 'cooldowns.json'),
|
|
44
|
+
];
|
|
45
|
+
for (const fp of stateFiles) {
|
|
46
|
+
try { shared.assertStateFileSize(fp); } catch (e) {
|
|
47
|
+
console.error('\n[dashboard] STARTUP ABORTED — ' + e.message + '\n');
|
|
48
|
+
process.exit(78); // 78 = configuration error; consistent with spawn-agent.js
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
|
|
35
53
|
const PORT = parseInt(process.env.PORT || process.argv[2]) || 7331;
|
|
36
54
|
let CONFIG = queries.getConfig();
|
|
37
55
|
let PROJECTS = _getProjects(CONFIG);
|
package/engine/cli.js
CHANGED
|
@@ -51,6 +51,18 @@ function handleCommand(cmd, args) {
|
|
|
51
51
|
|
|
52
52
|
const commands = {
|
|
53
53
|
start() {
|
|
54
|
+
// Startup state-file size guard (#1167): dispatch.json / cooldowns.json
|
|
55
|
+
// bloated past 100 MB silently OOMed V8 on JSON.parse at startup. Fail fast
|
|
56
|
+
// with an actionable message instead.
|
|
57
|
+
try {
|
|
58
|
+
for (const fp of [DISPATCH_PATH, path.join(ENGINE_DIR, 'cooldowns.json')]) {
|
|
59
|
+
shared.assertStateFileSize(fp);
|
|
60
|
+
}
|
|
61
|
+
} catch (stateErr) {
|
|
62
|
+
console.error('\n[engine] STARTUP ABORTED — ' + stateErr.message + '\n');
|
|
63
|
+
process.exit(78); // 78 = configuration error
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
// Run preflight checks (warn but don't block — engine may still be useful)
|
|
55
67
|
try {
|
|
56
68
|
const { runPreflight, printPreflight } = require('./preflight');
|
package/engine/cooldown.js
CHANGED
|
@@ -10,6 +10,31 @@ const queries = require('./queries');
|
|
|
10
10
|
const { safeJson, safeWrite, log, ENGINE_DEFAULTS } = shared;
|
|
11
11
|
const { ENGINE_DIR } = queries;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Truncate any string fields on a pendingContexts entry so a single huge PR
|
|
15
|
+
* comment / build log cannot bloat cooldowns.json to hundreds of MB (#1167).
|
|
16
|
+
* Returns a new entry object (does not mutate the caller's copy).
|
|
17
|
+
*/
|
|
18
|
+
function _truncateContextEntry(entry, maxBytes) {
|
|
19
|
+
if (entry == null) return entry;
|
|
20
|
+
const limit = Number(maxBytes) > 0 ? Number(maxBytes) : ENGINE_DEFAULTS.maxPendingContextEntryBytes;
|
|
21
|
+
if (typeof entry === 'string') {
|
|
22
|
+
return Buffer.byteLength(entry, 'utf8') > limit
|
|
23
|
+
? entry.slice(0, limit) + `\n\n... [truncated: context exceeded ${Math.round(limit / 1024)} KB]`
|
|
24
|
+
: entry;
|
|
25
|
+
}
|
|
26
|
+
if (typeof entry !== 'object') return entry;
|
|
27
|
+
const out = Array.isArray(entry) ? [] : {};
|
|
28
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
29
|
+
if (typeof v === 'string' && Buffer.byteLength(v, 'utf8') > limit) {
|
|
30
|
+
out[k] = v.slice(0, limit) + `\n\n... [truncated: ${k} exceeded ${Math.round(limit / 1024)} KB]`;
|
|
31
|
+
} else {
|
|
32
|
+
out[k] = v;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
const COOLDOWN_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
14
39
|
const dispatchCooldowns = new Map(); // key → { timestamp, failures }
|
|
15
40
|
|
|
@@ -39,9 +64,15 @@ function saveCooldowns() {
|
|
|
39
64
|
}
|
|
40
65
|
// Trim pendingContexts arrays before writing to prevent bloat
|
|
41
66
|
const cap = ENGINE_DEFAULTS.maxPendingContexts;
|
|
67
|
+
const entryLimit = ENGINE_DEFAULTS.maxPendingContextEntryBytes;
|
|
42
68
|
for (const [, v] of dispatchCooldowns) {
|
|
43
|
-
if (Array.isArray(v.pendingContexts)
|
|
44
|
-
v.pendingContexts
|
|
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));
|
|
45
76
|
}
|
|
46
77
|
}
|
|
47
78
|
const obj = Object.fromEntries(dispatchCooldowns);
|
|
@@ -69,7 +100,12 @@ function setCooldown(key) {
|
|
|
69
100
|
function setCooldownWithContext(key, context) {
|
|
70
101
|
const existing = dispatchCooldowns.get(key);
|
|
71
102
|
let pendingContexts = existing?.pendingContexts || [];
|
|
72
|
-
if (context)
|
|
103
|
+
if (context) {
|
|
104
|
+
// Truncate oversized string fields per-entry BEFORE storing so a single
|
|
105
|
+
// huge payload (PR diff, build log, full repo dump) cannot bloat
|
|
106
|
+
// cooldowns.json regardless of array cap (#1167).
|
|
107
|
+
pendingContexts.push(_truncateContextEntry(context, ENGINE_DEFAULTS.maxPendingContextEntryBytes));
|
|
108
|
+
}
|
|
73
109
|
// Cap to last N entries to prevent unbounded growth (cooldowns.json bloat)
|
|
74
110
|
const cap = ENGINE_DEFAULTS.maxPendingContexts;
|
|
75
111
|
if (pendingContexts.length > cap) pendingContexts = pendingContexts.slice(-cap);
|
package/engine/dispatch.js
CHANGED
|
@@ -11,6 +11,7 @@ const { setCooldownFailure } = require('./cooldown');
|
|
|
11
11
|
|
|
12
12
|
const { safeJson, safeWrite, safeReadDir, mutateJsonFileLocked, mutateWorkItems,
|
|
13
13
|
mutatePullRequests, getProjects, projectWorkItemsPath, projectPrPath, log, ts, dateStamp,
|
|
14
|
+
sidecarDispatchPrompt, deleteDispatchPromptSidecar,
|
|
14
15
|
WI_STATUS, DISPATCH_RESULT, ENGINE_DEFAULTS, AGENT_STATUS, FAILURE_CLASS } = shared;
|
|
15
16
|
const { getConfig, getDispatch, DISPATCH_PATH, INBOX_DIR } = queries;
|
|
16
17
|
|
|
@@ -24,13 +25,34 @@ function recovery() { if (!_recovery) _recovery = require('./recovery'); return
|
|
|
24
25
|
|
|
25
26
|
// ─── Dispatch Mutation ───────────────────────────────────────────────────────
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Sweep pending + active dispatch entries and move any oversized prompts to
|
|
30
|
+
* sidecar files. Keeps dispatch.json from bloating to hundreds of MB when
|
|
31
|
+
* fix-type prompts inline PR diffs / build logs / coalesced feedback (#1167).
|
|
32
|
+
* Safe to call on every mutation: small prompts are untouched.
|
|
33
|
+
*/
|
|
34
|
+
function _sidecarOversizedPrompts(dispatch) {
|
|
35
|
+
const threshold = ENGINE_DEFAULTS.maxDispatchPromptBytes;
|
|
36
|
+
const lists = [dispatch.pending, dispatch.active];
|
|
37
|
+
for (const list of lists) {
|
|
38
|
+
if (!Array.isArray(list)) continue;
|
|
39
|
+
for (const item of list) {
|
|
40
|
+
if (item && typeof item.prompt === 'string') sidecarDispatchPrompt(item, threshold);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
function mutateDispatch(mutator) {
|
|
28
46
|
const defaultDispatch = { pending: [], active: [], completed: [] };
|
|
29
47
|
const result = mutateJsonFileLocked(DISPATCH_PATH, (dispatch) => {
|
|
30
48
|
dispatch.pending = Array.isArray(dispatch.pending) ? dispatch.pending : [];
|
|
31
49
|
dispatch.active = Array.isArray(dispatch.active) ? dispatch.active : [];
|
|
32
50
|
dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
|
|
33
|
-
|
|
51
|
+
const next = mutator(dispatch) ?? dispatch;
|
|
52
|
+
// Prompt-size guard: runs on every write so a single bad item cannot bloat
|
|
53
|
+
// dispatch.json. Sidecars live in engine/contexts/<id>.md.
|
|
54
|
+
_sidecarOversizedPrompts(next);
|
|
55
|
+
return next;
|
|
34
56
|
}, { defaultValue: defaultDispatch });
|
|
35
57
|
// Invalidate the read cache so next getDispatch() sees fresh data
|
|
36
58
|
try { require('./queries').invalidateDispatchCache(); } catch {}
|
|
@@ -116,7 +138,12 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
116
138
|
if (reason) item.reason = reason;
|
|
117
139
|
if (resultSummary) item.resultSummary = resultSummary;
|
|
118
140
|
if (failureClass && result === DISPATCH_RESULT.ERROR) item.failureClass = failureClass;
|
|
141
|
+
// Drop prompt (and sidecar file, if any) — completed entries don't need
|
|
142
|
+
// replayable content and it would accumulate forever (#1167).
|
|
143
|
+
try { deleteDispatchPromptSidecar(item); } catch { /* best-effort */ }
|
|
119
144
|
delete item.prompt;
|
|
145
|
+
delete item._promptFile;
|
|
146
|
+
delete item._promptBytes;
|
|
120
147
|
if (dispatch.completed.length >= 100) {
|
|
121
148
|
dispatch.completed = dispatch.completed.slice(-100);
|
|
122
149
|
}
|
package/engine/shared.js
CHANGED
|
@@ -162,6 +162,118 @@ function safeUnlink(p) {
|
|
|
162
162
|
try { fs.unlinkSync(p); } catch { /* cleanup */ }
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// ── Dispatch Prompt Sidecar (#1167) ─────────────────────────────────────────
|
|
166
|
+
// Large prompts (PR diffs, build error logs, coalesced human feedback) inlined
|
|
167
|
+
// into dispatch.json caused hundreds-of-MB bloat per entry and eventual V8 OOM
|
|
168
|
+
// at startup. Sidecar files keep dispatch.json small while preserving full
|
|
169
|
+
// content for the agent at spawn time.
|
|
170
|
+
|
|
171
|
+
// Resolve lazily so MINIONS_TEST_DIR overrides work in tests.
|
|
172
|
+
function _promptContextsDir() {
|
|
173
|
+
return path.join(MINIONS_DIR, 'engine', 'contexts');
|
|
174
|
+
}
|
|
175
|
+
// Keep the constant for callers that expect a stable export; callers that need
|
|
176
|
+
// the current value (tests) should call _promptContextsDir().
|
|
177
|
+
const PROMPT_CONTEXTS_DIR = _promptContextsDir();
|
|
178
|
+
|
|
179
|
+
/** Absolute path to the sidecar prompt file for a given dispatch id. */
|
|
180
|
+
function dispatchPromptSidecarPath(dispatchId) {
|
|
181
|
+
if (!dispatchId) return null;
|
|
182
|
+
const safeId = String(dispatchId).replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
183
|
+
return path.join(_promptContextsDir(), `${safeId}.md`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* If the dispatch item's prompt exceeds thresholdBytes, write the full prompt
|
|
188
|
+
* to engine/contexts/<id>.md and replace `item.prompt` with a short stub
|
|
189
|
+
* + `_promptFile` reference. Mutates item in place and returns true when
|
|
190
|
+
* sidecaring happened, false otherwise.
|
|
191
|
+
*/
|
|
192
|
+
function sidecarDispatchPrompt(item, thresholdBytes) {
|
|
193
|
+
if (!item || typeof item.prompt !== 'string') return false;
|
|
194
|
+
const threshold = Number(thresholdBytes) > 0
|
|
195
|
+
? Number(thresholdBytes)
|
|
196
|
+
: ENGINE_DEFAULTS.maxDispatchPromptBytes;
|
|
197
|
+
const byteLen = Buffer.byteLength(item.prompt, 'utf8');
|
|
198
|
+
if (byteLen <= threshold) return false;
|
|
199
|
+
if (!item.id) return false; // can't sidecar without a stable id
|
|
200
|
+
try {
|
|
201
|
+
const ctxDir = _promptContextsDir();
|
|
202
|
+
if (!fs.existsSync(ctxDir)) fs.mkdirSync(ctxDir, { recursive: true });
|
|
203
|
+
const sidecar = dispatchPromptSidecarPath(item.id);
|
|
204
|
+
safeWrite(sidecar, item.prompt);
|
|
205
|
+
const relPath = path.relative(MINIONS_DIR, sidecar).replace(/\\/g, '/');
|
|
206
|
+
item._promptFile = relPath;
|
|
207
|
+
item._promptBytes = byteLen;
|
|
208
|
+
item.prompt = `[Prompt sidecarred to ${relPath} — ${Math.round(byteLen / 1024)} KB. The engine reads the sidecar when spawning this agent.]`;
|
|
209
|
+
try { log('warn', `Sidecarred oversized dispatch prompt: ${item.id} (${Math.round(byteLen / 1024)} KB → ${relPath})`); } catch { /* logger may not be ready */ }
|
|
210
|
+
return true;
|
|
211
|
+
} catch (e) {
|
|
212
|
+
try { log('warn', `sidecarDispatchPrompt failed for ${item.id}: ${e.message}`); } catch { /* cleanup */ }
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Read the effective prompt for a dispatch item. Prefers the sidecar file when
|
|
219
|
+
* `_promptFile` is set so spawnAgent always sees the full prompt even though
|
|
220
|
+
* dispatch.json only stores a small stub.
|
|
221
|
+
*/
|
|
222
|
+
function resolveDispatchPrompt(item) {
|
|
223
|
+
if (!item) return '';
|
|
224
|
+
if (item._promptFile) {
|
|
225
|
+
const candidates = [
|
|
226
|
+
path.isAbsolute(item._promptFile) ? item._promptFile : path.resolve(MINIONS_DIR, item._promptFile),
|
|
227
|
+
dispatchPromptSidecarPath(item.id),
|
|
228
|
+
].filter(Boolean);
|
|
229
|
+
for (const c of candidates) {
|
|
230
|
+
try {
|
|
231
|
+
const content = fs.readFileSync(c, 'utf8');
|
|
232
|
+
if (content) return content;
|
|
233
|
+
} catch { /* try next candidate */ }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return item.prompt || '';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Remove the sidecar prompt file for a completed/cancelled dispatch. */
|
|
240
|
+
function deleteDispatchPromptSidecar(item) {
|
|
241
|
+
if (!item) return;
|
|
242
|
+
const paths = new Set();
|
|
243
|
+
if (item._promptFile) {
|
|
244
|
+
paths.add(path.isAbsolute(item._promptFile) ? item._promptFile : path.resolve(MINIONS_DIR, item._promptFile));
|
|
245
|
+
}
|
|
246
|
+
const idPath = dispatchPromptSidecarPath(item.id);
|
|
247
|
+
if (idPath) paths.add(idPath);
|
|
248
|
+
for (const p of paths) safeUnlink(p);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Startup guard: throw a clear error when a state file has grown past
|
|
253
|
+
* maxStateFileBytes. Without this the dashboard silently OOMs on JSON.parse
|
|
254
|
+
* (seen on a 491 MB dispatch.json + 509 MB cooldowns.json — #1167).
|
|
255
|
+
* The thrown error points at the bloated file so operators can act instead
|
|
256
|
+
* of chasing V8 heap traces.
|
|
257
|
+
*/
|
|
258
|
+
function assertStateFileSize(filePath, maxBytes) {
|
|
259
|
+
const limit = Number(maxBytes) > 0 ? Number(maxBytes) : ENGINE_DEFAULTS.maxStateFileBytes;
|
|
260
|
+
try {
|
|
261
|
+
const stat = fs.statSync(filePath);
|
|
262
|
+
if (stat.size > limit) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`State file too large: ${filePath} is ${Math.round(stat.size / (1024 * 1024))} MB ` +
|
|
265
|
+
`(limit ${Math.round(limit / (1024 * 1024))} MB). ` +
|
|
266
|
+
`This usually means dispatch prompts or cooldown contexts were inlined and not sidecarred. ` +
|
|
267
|
+
`Inspect/trim the file manually, then restart. See engine/contexts/ for sidecar files.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
if (e.code === 'ENOENT') return; // file absent is fine
|
|
272
|
+
if (e.message && e.message.startsWith('State file too large:')) throw e;
|
|
273
|
+
// Other stat errors (permission etc.) — do not block startup
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
165
277
|
function sleepMs(ms) {
|
|
166
278
|
try {
|
|
167
279
|
const ab = new SharedArrayBuffer(4);
|
|
@@ -571,6 +683,9 @@ const ENGINE_DEFAULTS = {
|
|
|
571
683
|
ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
|
|
572
684
|
heartbeatTimeouts: {}, // populated after WORK_TYPE is defined (below)
|
|
573
685
|
maxPendingContexts: 20, // cap pendingContexts arrays in cooldowns.json to prevent unbounded growth
|
|
686
|
+
maxPendingContextEntryBytes: 256 * 1024, // 256 KB — cap each pendingContexts entry to prevent huge PR comments from bloating cooldowns.json
|
|
687
|
+
maxDispatchPromptBytes: 1024 * 1024, // 1 MB — dispatch items with prompts larger than this sidecar to engine/contexts/ to prevent dispatch.json OOM (#1167)
|
|
688
|
+
maxStateFileBytes: 100 * 1024 * 1024, // 100 MB — fail startup with a clear error when dispatch.json / cooldowns.json exceed this, rather than silently OOMing on JSON.parse (#1167)
|
|
574
689
|
ccMaxTurns: 50, // max tool-use turns for CC/doc-chat before CLI stops
|
|
575
690
|
// Teams integration — config.teams shape: { enabled, appId, appPassword, certPath, privateKeyPath, tenantId, notifyEvents, ccMirror, inboxPollInterval }
|
|
576
691
|
// Auth modes: (1) appId + appPassword (client secret), or (2) appId + certPath + privateKeyPath + tenantId (certificate)
|
|
@@ -1399,6 +1514,12 @@ module.exports = {
|
|
|
1399
1514
|
safeJson, safeJsonObj, safeJsonArr,
|
|
1400
1515
|
safeWrite,
|
|
1401
1516
|
safeUnlink,
|
|
1517
|
+
PROMPT_CONTEXTS_DIR,
|
|
1518
|
+
dispatchPromptSidecarPath,
|
|
1519
|
+
sidecarDispatchPrompt,
|
|
1520
|
+
resolveDispatchPrompt,
|
|
1521
|
+
deleteDispatchPromptSidecar,
|
|
1522
|
+
assertStateFileSize,
|
|
1402
1523
|
withFileLock,
|
|
1403
1524
|
mutateJsonFileLocked,
|
|
1404
1525
|
mutateWorkItems,
|
package/engine.js
CHANGED
|
@@ -355,7 +355,11 @@ async function recoverPartialWorktree(rootDir, worktreePath, branchName, gitOpts
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
async function spawnAgent(dispatchItem, config) {
|
|
358
|
-
const { id, agent: agentId,
|
|
358
|
+
const { id, agent: agentId, type, meta } = dispatchItem;
|
|
359
|
+
// Resolve prompt — prefers sidecar file when dispatchItem._promptFile is set
|
|
360
|
+
// (large prompts are written to engine/contexts/<id>.md to keep dispatch.json
|
|
361
|
+
// small — see shared.sidecarDispatchPrompt / #1167).
|
|
362
|
+
const taskPrompt = shared.resolveDispatchPrompt(dispatchItem);
|
|
359
363
|
const claudeConfig = config.claude || {};
|
|
360
364
|
const engineConfig = config.engine || {};
|
|
361
365
|
const startedAt = ts();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1036",
|
|
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"
|
package/playbooks/test.md
CHANGED
|
@@ -17,14 +17,15 @@ Team root: {{team_root}}
|
|
|
17
17
|
|
|
18
18
|
## Instructions
|
|
19
19
|
|
|
20
|
-
This is a **test/build/run task**. Your goal is to build, run, test, or verify something
|
|
20
|
+
This is a **test/build/run task**. Your goal is to build, run, test, or verify something.
|
|
21
21
|
|
|
22
22
|
1. **Navigate** to the correct project directory
|
|
23
23
|
2. You are already in the correct working directory. If you need a specific branch, use `git checkout` — do NOT create additional worktrees.
|
|
24
24
|
3. **Build** the project — follow the repo's build instructions (check CLAUDE.md, package.json, README)
|
|
25
25
|
4. **Run** if the task asks for it (e.g., `yarn start`, `yarn dev`, docker-compose, etc.)
|
|
26
26
|
5. **Test** if the task asks for it (e.g., `yarn test`, `pytest`, etc.)
|
|
27
|
-
6. **
|
|
27
|
+
6. **Write tests** if the task asks you to add or improve test coverage — commit and open a PR when done
|
|
28
|
+
7. **Report results** — what worked, what failed, build output, test results, localhost URL if running
|
|
28
29
|
|
|
29
30
|
## Working Style
|
|
30
31
|
|
|
@@ -32,9 +33,9 @@ Use subagents only for genuinely parallel, independent tasks. For building, test
|
|
|
32
33
|
|
|
33
34
|
## Rules
|
|
34
35
|
|
|
35
|
-
- **Do NOT
|
|
36
|
-
- **
|
|
37
|
-
- **Do NOT
|
|
36
|
+
- **Do NOT modify production code** unless the task explicitly asks for a fix
|
|
37
|
+
- **Create a PR** if the task involves writing or modifying files (new tests, test fixes, etc.) — follow the same PR conventions as implement tasks
|
|
38
|
+
- **Do NOT push or create a PR** for pure build/run/verify tasks that make no file changes
|
|
38
39
|
- Use PowerShell for build commands on Windows if applicable
|
|
39
40
|
- If a build or test fails, report the error clearly — don't try to fix it unless asked
|
|
40
41
|
- If running a local server, report the URL (e.g., http://localhost:3000)
|
|
@@ -66,6 +67,6 @@ Include:
|
|
|
66
67
|
|
|
67
68
|
## When to Stop
|
|
68
69
|
|
|
69
|
-
Your task is complete once you have run the tests, written findings to the inbox file, and (if
|
|
70
|
+
Your task is complete once you have run the tests, written findings to the inbox file, and (if the task involved file changes) created and submitted a PR. Stop after writing findings.
|
|
70
71
|
|
|
71
72
|
Do NOT remove worktrees — the engine handles cleanup automatically.
|