@yemi33/minions 0.1.2338 → 0.1.2339
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.js +75 -46
- package/package.json +1 -1
package/engine.js
CHANGED
|
@@ -10645,55 +10645,84 @@ async function tickInner() {
|
|
|
10645
10645
|
|
|
10646
10646
|
// Dispatch items — spawnAgent moves each from pending→active on disk.
|
|
10647
10647
|
// We use the already-loaded item objects; spawnAgent handles the state transition.
|
|
10648
|
+
//
|
|
10649
|
+
// W-mr9o1oe0001c4601 — Spawn all claimed items for this tick CONCURRENTLY
|
|
10650
|
+
// rather than awaiting each spawnAgent serially. `spawnAgent` does real
|
|
10651
|
+
// wall-clock work per item (prompt build + `git worktree add`, which on a
|
|
10652
|
+
// ~250k-file monorepo can take multiple seconds), so the old sequential loop
|
|
10653
|
+
// made eager routing (engine/routing.js already fans work across every idle
|
|
10654
|
+
// agent in a single allocation pass) visibly "trickle in" one agent at a time,
|
|
10655
|
+
// contradicting the stated eager-dispatch design. `toDispatch` is already
|
|
10656
|
+
// bounded by `slotsAvailable` (≤ maxConcurrent — the seed loop above decrements
|
|
10657
|
+
// `generalSlots` and stops pushing at 0), so an unbounded Promise.allSettled
|
|
10658
|
+
// here is naturally capped; no dedicated spawn-concurrency knob is needed.
|
|
10659
|
+
// Concurrent `git worktree add` against the same repo is safe on this codebase:
|
|
10660
|
+
// each worktree gets its own `.git/worktrees/<name>` metadata dir, and
|
|
10661
|
+
// runWorktreeAdd already retries the exact lock-contention errors
|
|
10662
|
+
// (`index.lock`, `could not lock`, `resource busy`, `already exists`) that
|
|
10663
|
+
// concurrent adds can transiently produce. The per-project live-checkout gate
|
|
10664
|
+
// (liveProjectsInUse) already caps live-mode projects to one mutating dispatch,
|
|
10665
|
+
// so concurrent in-place writes to one operator checkout cannot happen here.
|
|
10648
10666
|
const dispatched = new Set();
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10667
|
+
const spawnOneItem = async (item) => {
|
|
10668
|
+
// Dedup by id (toDispatch is already deduped by seenPendingIds — defensive).
|
|
10669
|
+
if (dispatched.has(item.id)) return;
|
|
10670
|
+
// Staleness: if a newer tick force-released this one while we were building
|
|
10671
|
+
// the batch, don't spawn a stale tick's agents.
|
|
10672
|
+
if (_isTickStale(myGeneration)) return;
|
|
10673
|
+
let proc;
|
|
10674
|
+
try { proc = await spawnAgent(item, config); } catch (spawnErr) {
|
|
10675
|
+
log('error', `spawnAgent exception for ${item.id}: ${spawnErr.message}`);
|
|
10676
|
+
proc = null;
|
|
10677
|
+
}
|
|
10678
|
+
// A newer tick may have superseded us mid-spawn — don't mutate shared state.
|
|
10679
|
+
if (_isTickStale(myGeneration)) return;
|
|
10680
|
+
if (proc === null) {
|
|
10681
|
+
// spawnAgent failed (e.g., worktree creation error). It already called
|
|
10682
|
+
// completeDispatch internally which handles retry logic, but log at the
|
|
10683
|
+
// dispatch-loop level for visibility and handle any edge cases where
|
|
10684
|
+
// completeDispatch wasn't called.
|
|
10685
|
+
log('error', `spawnAgent returned null for ${item.id} (${item.type} → ${item.agent}) — spawn failed`);
|
|
10686
|
+
// Defensive: ensure the work item is re-queued if completeDispatch didn't fire
|
|
10687
|
+
if (item.meta?.item?.id) {
|
|
10688
|
+
try {
|
|
10689
|
+
const wiPath = item.meta.source === 'central-work-item' || item.meta.source === 'central-work-item-fanout'
|
|
10690
|
+
? path.join(ENGINE_DIR, '..', 'work-items.json')
|
|
10691
|
+
: item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
|
|
10692
|
+
if (wiPath) {
|
|
10693
|
+
if (_isTickStale(myGeneration)) return;
|
|
10694
|
+
mutateWorkItems(wiPath, items => {
|
|
10695
|
+
const wi = items.find(i => i.id === item.meta.item.id);
|
|
10696
|
+
if (wi && wi.status === WI_STATUS.DISPATCHED) {
|
|
10697
|
+
// completeDispatch didn't update the work item — re-queue manually
|
|
10698
|
+
wi.status = WI_STATUS.PENDING;
|
|
10699
|
+
wi._retryCount = (wi._retryCount || 0) + 1;
|
|
10700
|
+
wi._lastRetryReason = 'spawnAgent returned null';
|
|
10701
|
+
wi._lastRetryAt = ts();
|
|
10702
|
+
delete wi.dispatched_at;
|
|
10703
|
+
delete wi.dispatched_to;
|
|
10704
|
+
log('info', `Re-queued ${item.meta.item.id} as pending (retry ${wi._retryCount})`);
|
|
10705
|
+
}
|
|
10706
|
+
});
|
|
10707
|
+
}
|
|
10708
|
+
} catch (e) { log('warn', `Failed to re-queue work item after spawn failure: ${e.message}`); }
|
|
10709
|
+
}
|
|
10710
|
+
} else {
|
|
10711
|
+
dispatched.add(item.id);
|
|
10712
|
+
// Sync PRD item status after successful spawn (#480)
|
|
10713
|
+
if (item.meta?.item?.sourcePlan) {
|
|
10714
|
+
try { syncPrdItemStatus(item.meta.item.id, WI_STATUS.DISPATCHED, item.meta.item.sourcePlan); }
|
|
10715
|
+
catch (e) { log('warn', `prd sync after spawn: ${e.message}`); }
|
|
10694
10716
|
}
|
|
10695
10717
|
}
|
|
10696
|
-
}
|
|
10718
|
+
};
|
|
10719
|
+
// Promise.allSettled: one item's spawn failure never affects another's, and
|
|
10720
|
+
// each spawnOneItem already swallows its own errors (per-item try/catch), so a
|
|
10721
|
+
// rejected task here is not expected — allSettled is belt-and-suspenders.
|
|
10722
|
+
await Promise.allSettled(toDispatch.map((item) => spawnOneItem(item)));
|
|
10723
|
+
// A force-release may have superseded this tick while spawns were in flight —
|
|
10724
|
+
// abort before the downstream skip-reason annotation reads/mutates shared state.
|
|
10725
|
+
if (_isTickStale(myGeneration)) return;
|
|
10697
10726
|
|
|
10698
10727
|
// Annotate remaining pending items with skipReason so dashboard can show why they're waiting.
|
|
10699
10728
|
// Re-read dispatch after spawns (spawnAgent moves items from pending→active).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2339",
|
|
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"
|