@yemi33/minions 0.1.2386 → 0.1.2388
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/dashboard.js +24 -20
- package/docs/command-center.md +1 -1
- package/engine/llm.js +12 -6
- package/engine/supervisor.js +48 -11
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -3570,18 +3570,23 @@ function _getCcLiveStream(tabId) {
|
|
|
3570
3570
|
function _touchCcLiveStream(state) {
|
|
3571
3571
|
if (!state) return;
|
|
3572
3572
|
state.updatedAt = Date.now();
|
|
3573
|
-
// W-mpmwxni2000c25c7-b — every CC streaming progress event
|
|
3574
|
-
//
|
|
3575
|
-
//
|
|
3576
|
-
//
|
|
3577
|
-
//
|
|
3578
|
-
// watchdog
|
|
3579
|
-
// it in `finally`, so late progress callbacks delivered after resolution
|
|
3580
|
-
// are a no-op.
|
|
3573
|
+
// W-mpmwxni2000c25c7-b — every CC streaming progress event routes through
|
|
3574
|
+
// here for the stall detector. The direct path also reports startup and
|
|
3575
|
+
// reasoning events through callLLMStreaming.onProgress, while the pool path
|
|
3576
|
+
// reports chunks and tools. Piggy-back the per-turn watchdog's bumpTimer on
|
|
3577
|
+
// the same heartbeat so active work cannot be killed by stale inactivity.
|
|
3578
|
+
// The watchdog removes `_bumpTimer` in `finally`, so late callbacks are no-op.
|
|
3581
3579
|
if (typeof state._bumpTimer === 'function') {
|
|
3582
3580
|
try { state._bumpTimer(); } catch { /* swallow */ }
|
|
3583
3581
|
}
|
|
3584
3582
|
}
|
|
3583
|
+
function _resolveCcDirectStreamSessionId({ sessionId, hasImages, useWorkerPool }) {
|
|
3584
|
+
// ACP session IDs belong to the live worker process. A one-off image CLI
|
|
3585
|
+
// cannot resume that session while the worker owns it, so rely on the
|
|
3586
|
+
// transcript carryover already included in the direct prompt.
|
|
3587
|
+
if (hasImages && useWorkerPool) return null;
|
|
3588
|
+
return sessionId || null;
|
|
3589
|
+
}
|
|
3585
3590
|
function _clearCcLiveTimers(tabId) {
|
|
3586
3591
|
const state = _getCcLiveStream(tabId);
|
|
3587
3592
|
if (!state) return;
|
|
@@ -9804,9 +9809,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9804
9809
|
// already-working --attachment path below, while image-less turns keep
|
|
9805
9810
|
// using the fast warm pool.
|
|
9806
9811
|
const hasImages = Array.isArray(images) && images.length > 0;
|
|
9807
|
-
|
|
9812
|
+
const useWorkerPool = shared.resolveCcUseWorkerPool(engineConfig);
|
|
9813
|
+
if (!hasImages && useWorkerPool) {
|
|
9808
9814
|
return _invokeCcStreamViaPool({ prompt, liveState, toolUses, model, effort, engineConfig, systemPrompt, tabId });
|
|
9809
9815
|
}
|
|
9816
|
+
const directSessionId = _resolveCcDirectStreamSessionId({ sessionId, hasImages, useWorkerPool });
|
|
9810
9817
|
const setStatus = (message) => {
|
|
9811
9818
|
liveState.statusMessage = message;
|
|
9812
9819
|
if (liveState.writer) liveState.writer({ type: 'status', message });
|
|
@@ -9819,10 +9826,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9819
9826
|
return callLLMStreaming(prompt, systemPrompt, {
|
|
9820
9827
|
timeout: CC_CALL_TIMEOUT_MS, label: 'command-center', model, maxTurns,
|
|
9821
9828
|
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
9822
|
-
sessionId, effort, direct: true,
|
|
9829
|
+
sessionId: directSessionId, effort, direct: true,
|
|
9823
9830
|
engineConfig, images,
|
|
9831
|
+
onProgress: () => _touchCcLiveStream(liveState),
|
|
9824
9832
|
onChunk: (text, segmentId) => {
|
|
9825
|
-
_touchCcLiveStream(liveState);
|
|
9826
9833
|
if (liveState.statusMessage) {
|
|
9827
9834
|
setStatus('');
|
|
9828
9835
|
}
|
|
@@ -9830,7 +9837,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9830
9837
|
if (liveState.writer) liveState.writer({ type: 'chunk', text, segmentId });
|
|
9831
9838
|
},
|
|
9832
9839
|
onToolUse: (name, input, id) => {
|
|
9833
|
-
_touchCcLiveStream(liveState);
|
|
9834
9840
|
if (liveState.statusMessage) {
|
|
9835
9841
|
setStatus('');
|
|
9836
9842
|
}
|
|
@@ -9842,7 +9848,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9842
9848
|
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input), id: id || null });
|
|
9843
9849
|
},
|
|
9844
9850
|
onToolUpdate: (id, status) => {
|
|
9845
|
-
_touchCcLiveStream(liveState);
|
|
9846
9851
|
// toolUses and liveState.tools share entry references — patch once so a
|
|
9847
9852
|
// reconnect replays the resolved (green/red) state, not just pending.
|
|
9848
9853
|
const entry = toolUses.find((e) => e && e.id === id);
|
|
@@ -10476,13 +10481,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10476
10481
|
timeoutMs: turnTimeoutMs, label: 'command-center-stream',
|
|
10477
10482
|
}, async (registerAbort, bumpTimer) => {
|
|
10478
10483
|
// W-mpmwxni2000c25c7-b — no-progress semantics: install bumpTimer
|
|
10479
|
-
// on liveState so _touchCcLiveStream
|
|
10480
|
-
//
|
|
10481
|
-
//
|
|
10482
|
-
//
|
|
10483
|
-
//
|
|
10484
|
-
// `finally` so a late progress event after resolution can't
|
|
10485
|
-
// re-arm against a stale abort target.
|
|
10484
|
+
// on liveState so _touchCcLiveStream resets the watchdog on every
|
|
10485
|
+
// liveness signal. Direct calls include runtime startup/reasoning
|
|
10486
|
+
// events; pool calls include chunks and tool updates. A productive
|
|
10487
|
+
// turn survives up to the outer CC_CALL_TIMEOUT_MS ceiling. Cleared
|
|
10488
|
+
// in `finally` so late progress cannot re-arm a stale abort target.
|
|
10486
10489
|
liveState._bumpTimer = bumpTimer;
|
|
10487
10490
|
try {
|
|
10488
10491
|
const llmPromise = _invokeCcStream({
|
|
@@ -14814,6 +14817,7 @@ module.exports = {
|
|
|
14814
14817
|
_ccRuntimeNeedsResumeCarryover,
|
|
14815
14818
|
_ccRuntimeNeedsResumeBookkeepingGuard,
|
|
14816
14819
|
_joinCcPromptParts,
|
|
14820
|
+
_resolveCcDirectStreamSessionId, // exported for testing
|
|
14817
14821
|
_captureApiRoutesMeta,
|
|
14818
14822
|
_resetPublicApiRoutesCacheForTest,
|
|
14819
14823
|
_formatCcApiRoutesIndex,
|
package/docs/command-center.md
CHANGED
|
@@ -70,7 +70,7 @@ Any violation rejects the **whole turn** with a typed error envelope (`code: 'in
|
|
|
70
70
|
|
|
71
71
|
**Worker-pool interaction (W-mray463s001zcee2).** The opt-in Copilot ACP worker pool (`engine.ccUseWorkerPool: true`, default ON for Copilot CC) is a **separate protocol path** from the table above — `engine/cc-worker-pool.js`'s `session/prompt` call only ever sends a single text content block (`[{ type: 'text', text: prompt }]`) and has no image/attachment content-block support today. This used to mean image turns were silently dropped whenever the pool was active, with a comment incorrectly blaming a Copilot capability gap (`imageInput:false`) that doesn't exist — Copilot's `imageInput` is `true` and fully supports images on the direct spawn path. The fix: `_invokeCcStream` (dashboard.js) now checks for a non-empty `images` array **before** routing through `resolveCcUseWorkerPool` — image-bearing turns bypass the warm pool for that turn only and cold-spawn a one-off CLI process via the working `--attachment` path (same as the non-pooled path always used), while image-less turns keep using the fast warm pool. If `engine/cc-worker-pool.js`'s ACP session protocol is later extended to carry attachments (Option A), this per-turn bypass in `_invokeCcStream` should be revisited/removed.
|
|
72
72
|
|
|
73
|
-
Because that bypass can spend tens of seconds starting a one-off process before the first model token, the SSE stream shows a user-focused “Looking at the image…” status instead of exposing the cold-start implementation detail or leaving an apparently stalled spinner.
|
|
73
|
+
Because that bypass can spend tens of seconds starting a one-off process before the first model token, the SSE stream shows a user-focused “Looking at the image…” status instead of exposing the cold-start implementation detail or leaving an apparently stalled spinner. The one-off process does not resume the live worker's process-scoped ACP session ID: it starts a transport-compatible fresh session and receives prior conversation context through the existing transcript carryover. Every parsed direct-runtime event, including startup and reasoning events, resets the five-minute silence watchdog; the safety bound therefore still catches a truly silent process without killing an image turn that is making progress.
|
|
74
74
|
|
|
75
75
|
`_resolveImageOpts` (in `engine/llm.js`) is pure and unit-tested: it forwards the `images` list only when `runtime.capabilities.imageInput` is truthy, otherwise returns the typed error so CC/doc-chat surface the envelope instead of a silently-text-only reply. `_spawnProcess` re-applies the same gate (`if (!caps.imageInput) adapterOpts.images = undefined`) as defense in depth. Image **filenames** are run through the untrusted-input fence (`buildSource('cc-image-filename', …)`) before they reach prompt text, since they are user-supplied.
|
|
76
76
|
|
package/engine/llm.js
CHANGED
|
@@ -482,6 +482,7 @@ function _createStreamAccumulator({
|
|
|
482
482
|
onTaskComplete = null,
|
|
483
483
|
onTerminalResult = null,
|
|
484
484
|
onThinking = null,
|
|
485
|
+
onProgress = null,
|
|
485
486
|
}) {
|
|
486
487
|
if (!runtime?.capabilities?.streamConsumer || typeof runtime.createStreamConsumer !== 'function') {
|
|
487
488
|
throw new Error(`runtime ${runtime?.name || '<unknown>'} missing createStreamConsumer (capabilities.streamConsumer)`);
|
|
@@ -600,6 +601,11 @@ function _createStreamAccumulator({
|
|
|
600
601
|
};
|
|
601
602
|
|
|
602
603
|
const consumer = runtime.createStreamConsumer(ctx);
|
|
604
|
+
function consumeEvent(ev) {
|
|
605
|
+
if (!ev) return;
|
|
606
|
+
if (onProgress) onProgress(ev);
|
|
607
|
+
consumer.consume(ev);
|
|
608
|
+
}
|
|
603
609
|
|
|
604
610
|
function ingestStdout(chunk) {
|
|
605
611
|
const str = chunk == null ? '' : chunk.toString();
|
|
@@ -608,8 +614,7 @@ function _createStreamAccumulator({
|
|
|
608
614
|
const lines = lineBuf.split('\n');
|
|
609
615
|
lineBuf = lines.pop() || '';
|
|
610
616
|
for (const line of lines) {
|
|
611
|
-
|
|
612
|
-
if (ev) consumer.consume(ev);
|
|
617
|
+
consumeEvent(runtime.parseStreamChunk(line));
|
|
613
618
|
}
|
|
614
619
|
}
|
|
615
620
|
|
|
@@ -619,10 +624,7 @@ function _createStreamAccumulator({
|
|
|
619
624
|
|
|
620
625
|
function finalize() {
|
|
621
626
|
const trimmed = lineBuf.trim();
|
|
622
|
-
if (trimmed)
|
|
623
|
-
const ev = runtime.parseStreamChunk(trimmed);
|
|
624
|
-
if (ev) consumer.consume(ev);
|
|
625
|
-
}
|
|
627
|
+
if (trimmed) consumeEvent(runtime.parseStreamChunk(trimmed));
|
|
626
628
|
if (!text && lastTaskCompleteSummary) text = lastTaskCompleteSummary;
|
|
627
629
|
// Reconciliation: always let the runtime adapter re-parse the complete
|
|
628
630
|
// stdout. Stream consumers emit live progress, but parseOutput() owns the
|
|
@@ -851,11 +853,14 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
851
853
|
* onChunk(text, segmentIndex) is called for each assistant text block as it
|
|
852
854
|
* arrives; segmentIndex increments across distinct text blocks so consumers can
|
|
853
855
|
* keep them visually separate instead of welding them together.
|
|
856
|
+
* onProgress(event) is called for every parsed runtime event, including startup
|
|
857
|
+
* and reasoning events that do not produce a text or tool callback.
|
|
854
858
|
*/
|
|
855
859
|
function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
856
860
|
const {
|
|
857
861
|
timeout = 120000, label = 'llm', maxTurns = 1, allowedTools = '',
|
|
858
862
|
sessionId = null, onChunk = () => {}, onToolUse = null, onToolUpdate = null,
|
|
863
|
+
onProgress = null,
|
|
859
864
|
effort = null, direct = false,
|
|
860
865
|
model: modelOverride, cli: cliOverride, engineConfig,
|
|
861
866
|
maxBudget, bare, fallbackModel,
|
|
@@ -903,6 +908,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
903
908
|
// 'exit'/'close' when an inherited pipe keeps the parent's FDs open.
|
|
904
909
|
onTerminalResult: () => scheduleExitFallback(exitCode != null ? exitCode : 0),
|
|
905
910
|
onThinking: opts.onThinking || null,
|
|
911
|
+
onProgress,
|
|
906
912
|
});
|
|
907
913
|
|
|
908
914
|
_abort = () => { shared.killImmediate(proc); };
|
package/engine/supervisor.js
CHANGED
|
@@ -507,7 +507,39 @@ function _isRespawnEligibleEngineState(state) {
|
|
|
507
507
|
return state === 'running' || state === 'degraded';
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
|
|
510
|
+
const ENGINE_WATCHDOG_OPERATION_NAMES = [
|
|
511
|
+
'isPidAlive',
|
|
512
|
+
'reapStrayProcesses',
|
|
513
|
+
'spawnEngine',
|
|
514
|
+
'recordCrashLoopRespawn',
|
|
515
|
+
];
|
|
516
|
+
|
|
517
|
+
// Isolated state does not isolate this module's source checkout, so tests must
|
|
518
|
+
// provide every OS/process side effect before an engine respawn is possible.
|
|
519
|
+
function _resolveEngineWatchdogOperations(injected) {
|
|
520
|
+
const provided = injected && typeof injected === 'object' ? injected : {};
|
|
521
|
+
if (process.env.MINIONS_TEST_DIR) {
|
|
522
|
+
const missing = ENGINE_WATCHDOG_OPERATION_NAMES
|
|
523
|
+
.filter(name => typeof provided[name] !== 'function');
|
|
524
|
+
if (missing.length > 0) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`MINIONS_TEST_DIR is set; inject engine watchdog operations instead of using real process control: ${missing.join(', ')}`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
isPidAlive: typeof provided.isPidAlive === 'function' ? provided.isPidAlive : isPidAlive,
|
|
532
|
+
reapStrayProcesses: typeof provided.reapStrayProcesses === 'function'
|
|
533
|
+
? provided.reapStrayProcesses
|
|
534
|
+
: reapStrayProcesses,
|
|
535
|
+
spawnEngine: typeof provided.spawnEngine === 'function' ? provided.spawnEngine : spawnEngine,
|
|
536
|
+
recordCrashLoopRespawn: typeof provided.recordCrashLoopRespawn === 'function'
|
|
537
|
+
? provided.recordCrashLoopRespawn
|
|
538
|
+
: _recordCrashLoopRespawn,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function checkEngine(now, operations) {
|
|
511
543
|
if (now - _lastEngineRespawnAt < POST_SPAWN_GRACE_MS) return;
|
|
512
544
|
const control = safeReadJson(CONTROL_PATH_FN());
|
|
513
545
|
// Respawn when control.json says "running" (PID died unexpectedly) or
|
|
@@ -515,7 +547,6 @@ function checkEngine(now) {
|
|
|
515
547
|
// above). "paused"/"stopped"/"stopping" are legitimate user-intent states the
|
|
516
548
|
// supervisor must not override.
|
|
517
549
|
if (!control || !_isRespawnEligibleEngineState(control.state)) return;
|
|
518
|
-
if (control.pid && isPidAlive(control.pid)) return;
|
|
519
550
|
|
|
520
551
|
// Cross-watchdog race guard: dashboard.js's in-process engine watchdog
|
|
521
552
|
// writes `{pid: null, restarted_at}` BEFORE spawning a new engine. During
|
|
@@ -530,13 +561,16 @@ function checkEngine(now) {
|
|
|
530
561
|
}
|
|
531
562
|
}
|
|
532
563
|
|
|
564
|
+
const ops = _resolveEngineWatchdogOperations(operations);
|
|
565
|
+
if (control.pid && ops.isPidAlive(control.pid)) return;
|
|
566
|
+
|
|
533
567
|
console.log(`[supervisor] Engine PID ${control.pid || '(none)'} is dead — respawning...`);
|
|
534
568
|
// Reap any orphan engines first so a stale control.json PID can't snowball
|
|
535
569
|
// into an unbounded pile of contending engine processes.
|
|
536
|
-
reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine');
|
|
537
|
-
const newPid = spawnEngine();
|
|
570
|
+
ops.reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine');
|
|
571
|
+
const newPid = ops.spawnEngine();
|
|
538
572
|
_lastEngineRespawnAt = now;
|
|
539
|
-
|
|
573
|
+
ops.recordCrashLoopRespawn('supervisor:dead-pid');
|
|
540
574
|
console.log(`[supervisor] Engine respawned (new PID: ${newPid})`);
|
|
541
575
|
}
|
|
542
576
|
|
|
@@ -546,7 +580,7 @@ function checkEngine(now) {
|
|
|
546
580
|
// is alive but control.heartbeat has not advanced for SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS.
|
|
547
581
|
// The heartbeat is written every ~15s by a dedicated timer in engine/cli.js; if
|
|
548
582
|
// it goes stale while the PID persists, the event loop is stuck.
|
|
549
|
-
function checkEngineHung(now) {
|
|
583
|
+
function checkEngineHung(now, operations) {
|
|
550
584
|
if (now - _lastEngineRespawnAt < POST_SPAWN_GRACE_MS) return;
|
|
551
585
|
const control = safeReadJson(CONTROL_PATH_FN());
|
|
552
586
|
// Include 'degraded' (see _isRespawnEligibleEngineState) so a permanently
|
|
@@ -554,8 +588,6 @@ function checkEngineHung(now) {
|
|
|
554
588
|
// re-enters) still gets force-restarted once its own heartbeat goes stale,
|
|
555
589
|
// instead of being ignored forever the moment the state flips to 'degraded'.
|
|
556
590
|
if (!control || !_isRespawnEligibleEngineState(control.state)) return;
|
|
557
|
-
// Only acts when the PID is alive — a dead PID is handled by checkEngine().
|
|
558
|
-
if (!control.pid || !isPidAlive(control.pid)) return;
|
|
559
591
|
|
|
560
592
|
const heartbeat = Number(control.heartbeat);
|
|
561
593
|
if (!heartbeat || !Number.isFinite(heartbeat)) return; // no heartbeat yet (fresh start)
|
|
@@ -563,15 +595,20 @@ function checkEngineHung(now) {
|
|
|
563
595
|
const heartbeatAge = now - heartbeat;
|
|
564
596
|
if (heartbeatAge < SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS) return;
|
|
565
597
|
|
|
598
|
+
// Only acts when the PID is alive — a dead PID is handled by checkEngine().
|
|
599
|
+
if (!control.pid) return;
|
|
600
|
+
const ops = _resolveEngineWatchdogOperations(operations);
|
|
601
|
+
if (!ops.isPidAlive(control.pid)) return;
|
|
602
|
+
|
|
566
603
|
console.log(
|
|
567
604
|
`[supervisor] Engine PID ${control.pid} is alive but heartbeat is stale ` +
|
|
568
605
|
`(${Math.round(heartbeatAge / 1000)}s old, threshold ${SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS / 1000}s) — ` +
|
|
569
606
|
`event loop likely frozen; restarting engine`,
|
|
570
607
|
);
|
|
571
|
-
reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine');
|
|
572
|
-
const newPid = spawnEngine();
|
|
608
|
+
ops.reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine');
|
|
609
|
+
const newPid = ops.spawnEngine();
|
|
573
610
|
_lastEngineRespawnAt = now;
|
|
574
|
-
|
|
611
|
+
ops.recordCrashLoopRespawn('supervisor:stale-heartbeat');
|
|
575
612
|
console.log(`[supervisor] Engine restarted due to stale heartbeat (new PID: ${newPid})`);
|
|
576
613
|
}
|
|
577
614
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2388",
|
|
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"
|