@yemi33/minions 0.1.2201 → 0.1.2202
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/engine/consolidation.js +56 -33
- package/engine.js +8 -1
- package/package.json +1 -1
package/engine/consolidation.js
CHANGED
|
@@ -16,6 +16,55 @@ const { getInboxFiles, getNotes, INBOX_DIR, ENGINE_DIR,
|
|
|
16
16
|
NOTES_PATH, KNOWLEDGE_DIR, ARCHIVE_DIR } = queries;
|
|
17
17
|
const { wrapUntrusted, buildSource } = require('./untrusted-fence');
|
|
18
18
|
|
|
19
|
+
// notes.md size cap. Consolidation appends to notes.md every cycle; without a
|
|
20
|
+
// cap it grows unbounded.
|
|
21
|
+
const NOTES_MAX_BYTES = 50000;
|
|
22
|
+
|
|
23
|
+
// DATA-LOSS GUARD: cap notes.md size WITHOUT silently discarding the pruned
|
|
24
|
+
// content. The previous behavior sliced notes.md at a section boundary and
|
|
25
|
+
// wrote the shorter string back with no backup — so once notes.md crossed the
|
|
26
|
+
// cap, every consolidation cycle permanently dropped the oldest human/agent
|
|
27
|
+
// broadcast notes. This trims to the cap AND appends every dropped section to
|
|
28
|
+
// an overflow archive (notes/archive/notes-overflow.md), so consolidated notes
|
|
29
|
+
// are always recoverable (consistent with how inbox notes are archived, not
|
|
30
|
+
// deleted). Both consolidation paths (LLM + regex fallback) route through it.
|
|
31
|
+
// Returns the trimmed notes string. Callers must already hold the notes.md lock.
|
|
32
|
+
function _capNotesPreservingOverflow(newContent) {
|
|
33
|
+
if (newContent.length <= NOTES_MAX_BYTES) return newContent;
|
|
34
|
+
let kept = newContent;
|
|
35
|
+
let dropped = '';
|
|
36
|
+
const lastBoundary = newContent.lastIndexOf('\n---\n\n### ', NOTES_MAX_BYTES);
|
|
37
|
+
if (lastBoundary > 0) {
|
|
38
|
+
kept = newContent.slice(0, lastBoundary);
|
|
39
|
+
dropped = newContent.slice(lastBoundary);
|
|
40
|
+
log('info', `Capped notes.md at section boundary (pos ${lastBoundary}); overflow archived`);
|
|
41
|
+
} else {
|
|
42
|
+
// No section boundary before the cap — fall back to keeping header + the
|
|
43
|
+
// most recent sections, archiving the middle.
|
|
44
|
+
const sections = newContent.split('\n---\n\n### ');
|
|
45
|
+
if (sections.length > 10) {
|
|
46
|
+
kept = sections[0] + '\n---\n\n### ' + sections.slice(-8).join('\n---\n\n### ');
|
|
47
|
+
const middle = sections.slice(1, -8);
|
|
48
|
+
if (middle.length) dropped = '\n---\n\n### ' + middle.join('\n---\n\n### ');
|
|
49
|
+
log('info', `Capped notes.md: ${middle.length} old section(s) archived to overflow`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (dropped.trim()) {
|
|
53
|
+
try {
|
|
54
|
+
const overflowPath = path.join(ARCHIVE_DIR, 'notes-overflow.md');
|
|
55
|
+
shared.mutateTextFileLocked(overflowPath, prev =>
|
|
56
|
+
(prev || '# notes.md overflow — auto-archived when notes.md exceeded its size cap\n')
|
|
57
|
+
+ '\n' + dropped.trim() + '\n', { defaultValue: '' });
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// If the archive write fails, DON'T trim — returning the full content
|
|
60
|
+
// keeps notes.md oversized but loses nothing. Loud warn so it's visible.
|
|
61
|
+
log('warn', `notes.md overflow archive failed; leaving notes.md uncapped to avoid loss: ${e.message}`);
|
|
62
|
+
return newContent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return kept;
|
|
66
|
+
}
|
|
67
|
+
|
|
19
68
|
// Per-agent memory files live under knowledge/agents/<agent>.md and are
|
|
20
69
|
// injected into individual agent prompts (in addition to the broadcast
|
|
21
70
|
// notes.md). See knowledge/agents/README.md for the convention.
|
|
@@ -900,28 +949,9 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
900
949
|
// Wrap read-modify-write in file lock to prevent race with concurrent consolidation or manual edits
|
|
901
950
|
shared.withFileLock(NOTES_PATH + '.lock', () => {
|
|
902
951
|
const current = getNotes() || '';
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
// Truncate on section boundary — scan backward for last \n# before byte limit
|
|
907
|
-
// Never cut mid-section to preserve readability
|
|
908
|
-
const limit = 50000;
|
|
909
|
-
const lastSectionBoundary = newContent.lastIndexOf('\n---\n\n### ', limit);
|
|
910
|
-
if (lastSectionBoundary > 0) {
|
|
911
|
-
newContent = newContent.slice(0, lastSectionBoundary);
|
|
912
|
-
log('info', `Pruned notes.md at section boundary (pos ${lastSectionBoundary}) to stay under ${limit} bytes`);
|
|
913
|
-
} else {
|
|
914
|
-
// Fallback: use the old section-count approach
|
|
915
|
-
const sections = newContent.split('\n---\n\n### ');
|
|
916
|
-
if (sections.length > 10) {
|
|
917
|
-
const header = sections[0];
|
|
918
|
-
const recent = sections.slice(-8);
|
|
919
|
-
newContent = header + '\n---\n\n### ' + recent.join('\n---\n\n### ');
|
|
920
|
-
log('info', `Pruned notes.md: removed ${sections.length - 9} old sections`);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
952
|
+
// DATA-LOSS GUARD: cap size but archive the overflow instead of
|
|
953
|
+
// silently discarding it (see _capNotesPreservingOverflow).
|
|
954
|
+
const newContent = _capNotesPreservingOverflow(current + entry);
|
|
925
955
|
safeWrite(NOTES_PATH, newContent);
|
|
926
956
|
});
|
|
927
957
|
classifyToKnowledgeBase(items, config);
|
|
@@ -1040,17 +1070,8 @@ function consolidateWithRegex(items, files, config) {
|
|
|
1040
1070
|
// Wrap read-modify-write in file lock to prevent race with concurrent consolidation or manual edits
|
|
1041
1071
|
shared.withFileLock(NOTES_PATH + '.lock', () => {
|
|
1042
1072
|
const current = getNotes() || '';
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const limit = 50000;
|
|
1046
|
-
const lastBoundary = newContent.lastIndexOf('\n---\n\n### ', limit);
|
|
1047
|
-
if (lastBoundary > 0) {
|
|
1048
|
-
newContent = newContent.slice(0, lastBoundary);
|
|
1049
|
-
} else {
|
|
1050
|
-
const sections = newContent.split('\n---\n\n### ');
|
|
1051
|
-
if (sections.length > 10) { newContent = sections[0] + '\n---\n\n### ' + sections.slice(-8).join('\n---\n\n### '); }
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1073
|
+
// DATA-LOSS GUARD: cap size but archive the overflow (see LLM path).
|
|
1074
|
+
const newContent = _capNotesPreservingOverflow(current + entry);
|
|
1054
1075
|
safeWrite(NOTES_PATH, newContent);
|
|
1055
1076
|
});
|
|
1056
1077
|
classifyToKnowledgeBase(items, config);
|
|
@@ -1222,4 +1243,6 @@ module.exports = {
|
|
|
1222
1243
|
consolidateWithLLM,
|
|
1223
1244
|
consolidateWithRegex,
|
|
1224
1245
|
archiveInboxFiles,
|
|
1246
|
+
_capNotesPreservingOverflow,
|
|
1247
|
+
NOTES_MAX_BYTES,
|
|
1225
1248
|
};
|
package/engine.js
CHANGED
|
@@ -1459,7 +1459,14 @@ async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOp
|
|
|
1459
1459
|
// the rename path would produce, just without the recovery dir.
|
|
1460
1460
|
let forceRemoved = false;
|
|
1461
1461
|
let forceRemoveError = null;
|
|
1462
|
-
|
|
1462
|
+
// DATA-LOSS GUARD (W-mq5rwwss): only force-remove when we captured HEAD above.
|
|
1463
|
+
// The backup ref written below (refs/minions/quarantine/...) is the only thing
|
|
1464
|
+
// preserving this worktree's unpushed commits once the tree is destroyed — and
|
|
1465
|
+
// it can only be written if `headSha` was read. If rev-parse HEAD failed (the
|
|
1466
|
+
// corrupted/locked-tree path), force-removing would destroy the sole pointer to
|
|
1467
|
+
// unpushed work. In that case skip the force-remove and fall through to the
|
|
1468
|
+
// env-blocked inbox alert so the operator can recover the tree by hand.
|
|
1469
|
+
if (renameError && ENGINE_DEFAULTS.quarantineForceRemoveFallback && headSha) {
|
|
1463
1470
|
log('warn', `_quarantineDirtyWorktree: rename failed after ${renameAttempts || ENGINE_DEFAULTS.quarantineRenameRetryAttempts} attempt(s) (${renameError.code || renameError.message}); falling back to git worktree remove --force`);
|
|
1464
1471
|
try {
|
|
1465
1472
|
await shared.shellSafeGit(['worktree', 'remove', '--force', worktreePath], { ...gitOpts, cwd: rootDir, timeout: 30000 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2202",
|
|
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"
|