@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.
Files changed (2) hide show
  1. package/engine.js +75 -46
  2. 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
- for (const item of toDispatch) {
10650
- if (!dispatched.has(item.id)) {
10651
- let proc;
10652
- try { proc = await spawnAgent(item, config); } catch (spawnErr) {
10653
- log('error', `spawnAgent exception for ${item.id}: ${spawnErr.message}`);
10654
- proc = null;
10655
- }
10656
- if (_isTickStale(myGeneration)) return;
10657
- if (proc === null) {
10658
- // spawnAgent failed (e.g., worktree creation error). It already called
10659
- // completeDispatch internally which handles retry logic, but log at the
10660
- // dispatch-loop level for visibility and handle any edge cases where
10661
- // completeDispatch wasn't called.
10662
- log('error', `spawnAgent returned null for ${item.id} (${item.type} → ${item.agent}) — spawn failed`);
10663
- // Defensive: ensure the work item is re-queued if completeDispatch didn't fire
10664
- if (item.meta?.item?.id) {
10665
- try {
10666
- const wiPath = item.meta.source === 'central-work-item' || item.meta.source === 'central-work-item-fanout'
10667
- ? path.join(ENGINE_DIR, '..', 'work-items.json')
10668
- : item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
10669
- if (wiPath) {
10670
- if (_isTickStale(myGeneration)) return;
10671
- mutateWorkItems(wiPath, items => {
10672
- const wi = items.find(i => i.id === item.meta.item.id);
10673
- if (wi && wi.status === WI_STATUS.DISPATCHED) {
10674
- // completeDispatch didn't update the work item — re-queue manually
10675
- wi.status = WI_STATUS.PENDING;
10676
- wi._retryCount = (wi._retryCount || 0) + 1;
10677
- wi._lastRetryReason = 'spawnAgent returned null';
10678
- wi._lastRetryAt = ts();
10679
- delete wi.dispatched_at;
10680
- delete wi.dispatched_to;
10681
- log('info', `Re-queued ${item.meta.item.id} as pending (retry ${wi._retryCount})`);
10682
- }
10683
- });
10684
- }
10685
- } catch (e) { log('warn', `Failed to re-queue work item after spawn failure: ${e.message}`); }
10686
- }
10687
- } else {
10688
- dispatched.add(item.id);
10689
- // Sync PRD item status after successful spawn (#480)
10690
- if (item.meta?.item?.sourcePlan) {
10691
- try { syncPrdItemStatus(item.meta.item.id, WI_STATUS.DISPATCHED, item.meta.item.sourcePlan); }
10692
- catch (e) { log('warn', `prd sync after spawn: ${e.message}`); }
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.2338",
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"