@yemi33/minions 0.1.560 → 0.1.562
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 +10 -0
- package/dashboard.js +12 -3
- package/engine/cooldown.js +16 -0
- package/engine.js +44 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.562 (2026-04-08)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- auto-reset CC session on resume failure, add error logging
|
|
7
|
+
|
|
8
|
+
## 0.1.561 (2026-04-08)
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
- add branch-level mutex to prevent concurrent dispatch to same branch (closes #493) (#498)
|
|
12
|
+
|
|
3
13
|
## 0.1.560 (2026-04-08)
|
|
4
14
|
|
|
5
15
|
### Features
|
package/dashboard.js
CHANGED
|
@@ -3368,9 +3368,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3368
3368
|
if (result.code !== 0 || !result.text) {
|
|
3369
3369
|
const debugInfo = result.code !== 0 ? `(exit code ${result.code})` : '(empty response)';
|
|
3370
3370
|
const stderrTail = (result.stderr || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3371
|
+
console.error(`[CC-stream] Failed: code=${result.code}, stderr=${(result.stderr || '').slice(0, 500)}, stdout_tail=${(result.raw || '').slice(-500)}`);
|
|
3372
|
+
// If resuming a session failed, auto-reset so next attempt starts fresh
|
|
3373
|
+
let retryHint;
|
|
3374
|
+
if (wasResume && result.code !== 0) {
|
|
3375
|
+
ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
3376
|
+
safeWrite(path.join(ENGINE_DIR, 'cc-session.json'), ccSession);
|
|
3377
|
+
retryHint = 'Session was reset — send your message again to start fresh.';
|
|
3378
|
+
} else {
|
|
3379
|
+
retryHint = ccSession.sessionId
|
|
3380
|
+
? 'Your session is still active — just send your message again to retry.'
|
|
3381
|
+
: 'Try clicking **New Session** and sending your message again.';
|
|
3382
|
+
}
|
|
3374
3383
|
res.write('data: ' + JSON.stringify({ type: 'done', text: `I had trouble processing that ${debugInfo}. ${stderrTail ? 'Detail: ' + stderrTail : ''}\n\n${retryHint}`, actions: [], sessionId: ccSession.sessionId }) + '\n\n');
|
|
3375
3384
|
res.end();
|
|
3376
3385
|
return;
|
package/engine/cooldown.js
CHANGED
|
@@ -103,6 +103,21 @@ function isAlreadyDispatched(key) {
|
|
|
103
103
|
return recentCompleted.some(d => d.meta?.dispatchKey === key);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Check if a branch is currently locked by an active dispatch.
|
|
108
|
+
* Returns the conflicting dispatch item, or null if the branch is free.
|
|
109
|
+
*/
|
|
110
|
+
function isBranchActive(branch) {
|
|
111
|
+
if (!branch) return null;
|
|
112
|
+
const { sanitizeBranch } = require('./shared');
|
|
113
|
+
const normalized = sanitizeBranch(branch);
|
|
114
|
+
const dispatch = queries.getDispatch();
|
|
115
|
+
return (dispatch.active || []).find(d => {
|
|
116
|
+
const dBranch = d.meta?.branch;
|
|
117
|
+
return dBranch && sanitizeBranch(dBranch) === normalized;
|
|
118
|
+
}) || null;
|
|
119
|
+
}
|
|
120
|
+
|
|
106
121
|
module.exports = {
|
|
107
122
|
COOLDOWN_PATH,
|
|
108
123
|
dispatchCooldowns,
|
|
@@ -114,4 +129,5 @@ module.exports = {
|
|
|
114
129
|
getCoalescedContexts,
|
|
115
130
|
setCooldownFailure,
|
|
116
131
|
isAlreadyDispatched,
|
|
132
|
+
isBranchActive,
|
|
117
133
|
};
|
package/engine.js
CHANGED
|
@@ -987,7 +987,7 @@ function updateSnapshot(config) {
|
|
|
987
987
|
|
|
988
988
|
const { COOLDOWN_PATH, dispatchCooldowns, loadCooldowns, saveCooldowns,
|
|
989
989
|
isOnCooldown, setCooldown, setCooldownWithContext, getCoalescedContexts,
|
|
990
|
-
setCooldownFailure, isAlreadyDispatched } = require('./engine/cooldown');
|
|
990
|
+
setCooldownFailure, isAlreadyDispatched, isBranchActive } = require('./engine/cooldown');
|
|
991
991
|
|
|
992
992
|
|
|
993
993
|
|
|
@@ -1399,6 +1399,11 @@ async function discoverFromPrs(config, project) {
|
|
|
1399
1399
|
for (const pr of prs) {
|
|
1400
1400
|
if (pr.status !== 'active') continue;
|
|
1401
1401
|
if (activePrIds.has(pr.id)) continue; // Skip PRs with active dispatch (prevent race)
|
|
1402
|
+
// Branch mutex: skip if PR branch is locked by any active dispatch (cross-type collision)
|
|
1403
|
+
if (pr.branch && isBranchActive(pr.branch)) {
|
|
1404
|
+
log('info', `Branch mutex: skipping PR ${pr.id} dispatch — branch ${pr.branch} locked by another agent`);
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1402
1407
|
// Skip human-authored PRs not linked to any work item — only auto-manage agent PRs
|
|
1403
1408
|
// Manually-linked PRs with autoObserve are allowed through (they have _autoObserve flag)
|
|
1404
1409
|
const isAgentPr = knownAgents.has((pr.agent || '').toLowerCase()) || (pr.prdItems && pr.prdItems.length > 0) || pr._autoObserve;
|
|
@@ -1678,6 +1683,16 @@ function discoverFromWorkItems(config, project) {
|
|
|
1678
1683
|
|
|
1679
1684
|
const isShared = item.branchStrategy === 'shared-branch' && item.featureBranch;
|
|
1680
1685
|
const branchName = isShared ? item.featureBranch : (item.branch || `work/${item.id}`);
|
|
1686
|
+
|
|
1687
|
+
// Branch mutex: skip if target branch is locked by an active dispatch
|
|
1688
|
+
const branchConflict = isBranchActive(branchName);
|
|
1689
|
+
if (branchConflict) {
|
|
1690
|
+
if (item._pendingReason !== 'branch_locked') { item._pendingReason = 'branch_locked'; needsWrite = true; }
|
|
1691
|
+
skipped.gated++;
|
|
1692
|
+
log('info', `Branch mutex: skipping ${item.id} — branch ${branchName} locked by ${branchConflict.id} (${branchConflict.agent})`);
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1681
1696
|
const vars = {
|
|
1682
1697
|
...buildBaseVars(agentId, config, project),
|
|
1683
1698
|
item_id: item.id,
|
|
@@ -2158,6 +2173,14 @@ function discoverCentralWorkItems(config) {
|
|
|
2158
2173
|
const firstProject = projects.length > 0 ? projects[0] : null;
|
|
2159
2174
|
if (!firstProject) { log('warn', `Dispatch: skipping ${item.id} — no projects configured`); continue; }
|
|
2160
2175
|
|
|
2176
|
+
// Branch mutex: skip if target branch is locked by an active dispatch
|
|
2177
|
+
const centralBranch = item.branch || item.featureBranch || `work/${item.id}`;
|
|
2178
|
+
const centralBranchConflict = isBranchActive(centralBranch);
|
|
2179
|
+
if (centralBranchConflict) {
|
|
2180
|
+
log('info', `Branch mutex: skipping central ${item.id} — branch ${centralBranch} locked by ${centralBranchConflict.id} (${centralBranchConflict.agent})`);
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2161
2184
|
const vars = {
|
|
2162
2185
|
...buildBaseVars(agentId, config, firstProject),
|
|
2163
2186
|
item_id: item.id,
|
|
@@ -2172,9 +2195,6 @@ function discoverCentralWorkItems(config) {
|
|
|
2172
2195
|
scope_section: buildProjectContext(projects, null, false, agentName, agentRole),
|
|
2173
2196
|
project_path: firstProject?.localPath || '',
|
|
2174
2197
|
};
|
|
2175
|
-
|
|
2176
|
-
// Build common vars: references, acceptance criteria, checkpoint, notes, task context
|
|
2177
|
-
const centralBranch = item.branch || `work/${item.id}`;
|
|
2178
2198
|
const centralWtPath = firstProject?.localPath
|
|
2179
2199
|
? path.resolve(firstProject.localPath, config.engine?.worktreeRoot || '../worktrees', centralBranch)
|
|
2180
2200
|
: '';
|
|
@@ -2639,6 +2659,11 @@ async function tickInner() {
|
|
|
2639
2659
|
|
|
2640
2660
|
// Build set of agents currently active (one task per agent at a time).
|
|
2641
2661
|
const busyAgents = new Set((dispatch.active || []).map(d => d.agent));
|
|
2662
|
+
// Branch mutex: track branches locked by active dispatches to prevent concurrent writes
|
|
2663
|
+
const lockedBranches = new Set();
|
|
2664
|
+
for (const d of (dispatch.active || [])) {
|
|
2665
|
+
if (d.meta?.branch) lockedBranches.add(sanitizeBranch(d.meta.branch));
|
|
2666
|
+
}
|
|
2642
2667
|
const seenPendingIds = new Set();
|
|
2643
2668
|
const toDispatch = [];
|
|
2644
2669
|
let generalSlots = slotsAvailable;
|
|
@@ -2649,12 +2674,16 @@ async function tickInner() {
|
|
|
2649
2674
|
continue;
|
|
2650
2675
|
}
|
|
2651
2676
|
if (busyAgents.has(item.agent)) continue;
|
|
2677
|
+
// Branch mutex: skip items targeting a branch already locked by an active or newly-dispatched task
|
|
2678
|
+
const itemBranch = item.meta?.branch ? sanitizeBranch(item.meta.branch) : null;
|
|
2679
|
+
if (itemBranch && lockedBranches.has(itemBranch)) continue;
|
|
2652
2680
|
// Items explicitly assigned to an agent bypass concurrency cap — dispatch if agent is free
|
|
2653
2681
|
const isExplicitAssignment = !!item.meta?.item?.agent;
|
|
2654
2682
|
if (!isExplicitAssignment && generalSlots <= 0) continue;
|
|
2655
2683
|
seenPendingIds.add(item.id);
|
|
2656
2684
|
toDispatch.push(item);
|
|
2657
2685
|
busyAgents.add(item.agent);
|
|
2686
|
+
if (itemBranch) lockedBranches.add(itemBranch);
|
|
2658
2687
|
if (!isExplicitAssignment) generalSlots--;
|
|
2659
2688
|
}
|
|
2660
2689
|
|
|
@@ -2713,6 +2742,11 @@ async function tickInner() {
|
|
|
2713
2742
|
const postDispatch = getDispatch();
|
|
2714
2743
|
const postBusyAgents = new Set((postDispatch.active || []).map(d => d.agent));
|
|
2715
2744
|
const postActiveCount = (postDispatch.active || []).length;
|
|
2745
|
+
// Rebuild locked branches from post-dispatch active set for skip-reason annotation
|
|
2746
|
+
const postLockedBranches = new Set();
|
|
2747
|
+
for (const d of (postDispatch.active || [])) {
|
|
2748
|
+
if (d.meta?.branch) postLockedBranches.add(sanitizeBranch(d.meta.branch));
|
|
2749
|
+
}
|
|
2716
2750
|
let skipReasonChanged = false;
|
|
2717
2751
|
for (const item of (postDispatch.pending || [])) {
|
|
2718
2752
|
let reason = null;
|
|
@@ -2720,6 +2754,12 @@ async function tickInner() {
|
|
|
2720
2754
|
reason = 'max_concurrency';
|
|
2721
2755
|
} else if (postBusyAgents.has(item.agent)) {
|
|
2722
2756
|
reason = 'agent_busy';
|
|
2757
|
+
} else {
|
|
2758
|
+
// Branch mutex: annotate items waiting for a branch to become free
|
|
2759
|
+
const pendingBranch = item.meta?.branch ? sanitizeBranch(item.meta.branch) : null;
|
|
2760
|
+
if (pendingBranch && postLockedBranches.has(pendingBranch)) {
|
|
2761
|
+
reason = 'branch_locked';
|
|
2762
|
+
}
|
|
2723
2763
|
}
|
|
2724
2764
|
if (item.skipReason !== reason) {
|
|
2725
2765
|
item.skipReason = reason;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.562",
|
|
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"
|