@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.
@@ -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
- let newContent = current + entry;
904
-
905
- if (newContent.length > 50000) {
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
- let newContent = current + entry;
1044
- if (newContent.length > 50000) {
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
- if (renameError && ENGINE_DEFAULTS.quarantineForceRemoveFallback) {
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.2201",
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"