@yemi33/minions 0.1.294 → 0.1.295

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 CHANGED
@@ -1,14 +1,16 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.294 (2026-04-03)
3
+ ## 0.1.295 (2026-04-03)
4
4
 
5
5
  ### Features
6
+ - fix engine.js race conditions — worktree TOCTOU, self-heal, dispatch dedup
6
7
  - fix dashboard.js race conditions, input validation, and watcher leaks
7
8
  - fix cleanup.js — worktree TOCTOU, readdirSync isolation, KB restore verify
8
9
  - harden shared.js — backup verification, lock TOCTOU, docs
9
10
  - all doc-chats use Sonnet with full tools (agent change)
10
11
 
11
12
  ### Fixes
13
+ - address PR-124 review feedback — safeJson regression, read-only lock, filter cleanup
12
14
  - address review feedback on PR-122
13
15
  - add behavioral tests for CRITICAL propagation and stale lock ENOENT
14
16
  - CRITICAL errors in safeJson now propagate to callers
package/engine/shared.js CHANGED
@@ -57,16 +57,12 @@ function safeJson(p) {
57
57
  // Verify the restored file matches expected content
58
58
  const verifyData = JSON.parse(fs.readFileSync(p, 'utf8'));
59
59
  if (JSON.stringify(verifyData) !== JSON.stringify(backupData)) {
60
- const errMsg = `[safeJson] CRITICAL: backup restore verification failed for ${p} — written data does not match backup`;
61
- console.error(errMsg);
62
- throw new Error(errMsg);
60
+ console.error(`[safeJson] CRITICAL: backup restore verification failed for ${p} — written data does not match backup`);
63
61
  }
64
62
  } catch (restoreErr) {
65
- // Re-throw CRITICAL errors so they propagate to callers
66
- if (restoreErr.message && restoreErr.message.includes('CRITICAL')) throw restoreErr;
67
- const errMsg = `[safeJson] CRITICAL: backup restore failed for ${p}: ${restoreErr.message}`;
68
- console.error(errMsg);
69
- throw new Error(errMsg);
63
+ // Restore-to-primary is best-effort backupData is already parsed and valid.
64
+ // Don't throw: disk-full / permission errors should not discard valid data.
65
+ console.error(`[safeJson] restore write failed for ${p}: ${restoreErr.message}`);
70
66
  }
71
67
  return backupData;
72
68
  } catch (outerErr) {
@@ -158,8 +154,7 @@ function withFileLock(lockPath, fn, {
158
154
  // ENOENT: another process deleted the lock between stat and unlink — safe to retry
159
155
  if (unlinkErr.code !== 'ENOENT') throw unlinkErr;
160
156
  }
161
- sleepMs(retryDelayMs); // avoid busy-loop on contention
162
- continue;
157
+ continue; // lock just removed — retry immediately
163
158
  }
164
159
  } catch (staleErr) {
165
160
  // ENOENT from statSync: lock file disappeared between EEXIST and stat — retry will succeed
package/engine.js CHANGED
@@ -91,6 +91,7 @@ const safeJson = shared.safeJson;
91
91
  const safeRead = shared.safeRead;
92
92
  const safeWrite = shared.safeWrite;
93
93
  const mutateJsonFileLocked = shared.mutateJsonFileLocked;
94
+ const withFileLock = shared.withFileLock;
94
95
 
95
96
  // ─── Dispatch Management (extracted to engine/dispatch.js) ───────────────────
96
97
 
@@ -352,10 +353,15 @@ function spawnAgent(dispatchItem, config) {
352
353
  if (alreadyUsed) {
353
354
  const existingWtPath = findExistingWorktree(rootDir, branchName);
354
355
  if (existingWtPath && fs.existsSync(existingWtPath)) {
355
- const dispatch = safeJson(DISPATCH_PATH) || {};
356
- const activelyUsed = (dispatch.active || []).some(d => {
357
- const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
358
- return dBranch === branchName && d.id !== id;
356
+ // Bug fix: read dispatch under file lock so check-and-act is atomic
357
+ // Uses withFileLock directly (read-only) no unnecessary disk write
358
+ let activelyUsed = false;
359
+ withFileLock(DISPATCH_PATH + '.lock', () => {
360
+ const dp = safeJson(DISPATCH_PATH) || {};
361
+ activelyUsed = (dp.active || []).some(d => {
362
+ const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
363
+ return dBranch === branchName && d.id !== id;
364
+ });
359
365
  });
360
366
  if (activelyUsed) {
361
367
  log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
@@ -2354,9 +2360,17 @@ async function tickInner() {
2354
2360
  // Only dispatch to agents that aren't already busy (one task per agent at a time).
2355
2361
  // Build set of agents currently active.
2356
2362
  const busyAgents = new Set((dispatch.active || []).map(d => d.agent));
2363
+ // Bug fix #14: deduplicate pending by dispatch ID to prevent double-dispatch.
2364
+ // This guards against the same item appearing twice in the in-memory pending array.
2365
+ const seenPendingIds = new Set();
2357
2366
  const toDispatch = [];
2358
2367
  for (const item of dispatch.pending) {
2359
2368
  if (toDispatch.length >= slotsAvailable) break;
2369
+ if (seenPendingIds.has(item.id)) {
2370
+ log('warn', `Duplicate dispatch ID ${item.id} in pending queue — skipping`);
2371
+ continue;
2372
+ }
2373
+ seenPendingIds.add(item.id);
2360
2374
  if (busyAgents.has(item.agent)) continue; // agent already has an active task
2361
2375
  toDispatch.push(item);
2362
2376
  busyAgents.add(item.agent); // mark busy for this dispatch round too
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.294",
3
+ "version": "0.1.295",
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"