@yemi33/minions 0.1.1802 → 0.1.1804

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,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1804 (2026-05-08)
4
+
5
+ ### Fixes
6
+ - prevent Copilot CC response truncation
7
+
8
+ ## 0.1.1803 (2026-05-08)
9
+
10
+ ### Other
11
+ - Guarantee Copilot steering delivery (#2225)
12
+
3
13
  ## 0.1.1800 (2026-05-08)
4
14
 
5
15
  ### Other
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-08T17:22:37.672Z"
4
+ "cachedAt": "2026-05-08T18:04:30.632Z"
5
5
  }
package/engine/llm.js CHANGED
@@ -22,7 +22,6 @@ const { resolveRuntime } = require('./runtimes');
22
22
 
23
23
  const MINIONS_DIR = shared.MINIONS_DIR;
24
24
  const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
25
- const COPILOT_TASK_COMPLETE_GRACE_MS = 15000;
26
25
  const MISSING_RUNTIME_EXIT_CODE = 78;
27
26
  // When the spawned process emits 'exit' but 'close' is delayed (a detached
28
27
  // grandchild inherited stdio), wait this long for trailing stdout data to
@@ -627,17 +626,6 @@ function callLLM(promptText, sysPromptText, opts = {}) {
627
626
  maxBudget, bare, fallbackModel,
628
627
  ...runtimeFeatureOpts,
629
628
  });
630
- let taskCompleteTimer = null;
631
- const scheduleTaskCompleteClose = () => {
632
- if (taskCompleteTimer) return;
633
- taskCompleteTimer = setTimeout(() => { try { shared.killImmediate(proc); } catch {} }, COPILOT_TASK_COMPLETE_GRACE_MS);
634
- };
635
- const clearTaskCompleteTimer = () => {
636
- if (taskCompleteTimer) {
637
- clearTimeout(taskCompleteTimer);
638
- taskCompleteTimer = null;
639
- }
640
- };
641
629
  let resolved = false;
642
630
  let exitFallbackTimer = null;
643
631
  let exitCode = null;
@@ -650,7 +638,6 @@ function callLLM(promptText, sysPromptText, opts = {}) {
650
638
  maxRawBytes: ENGINE_DEFAULTS.maxLlmRawBytes,
651
639
  maxStderrBytes: ENGINE_DEFAULTS.maxLlmStderrBytes,
652
640
  maxLineBufferBytes: ENGINE_DEFAULTS.maxLlmLineBufferBytes,
653
- onTaskComplete: scheduleTaskCompleteClose,
654
641
  // Terminal text from the runtime adapter signals the LLM has logically
655
642
  // completed — kick the drain timer so we don't block on a delayed
656
643
  // 'exit'/'close' when an inherited pipe keeps the parent's FDs open.
@@ -668,7 +655,6 @@ function callLLM(promptText, sysPromptText, opts = {}) {
668
655
  if (resolved) return;
669
656
  resolved = true;
670
657
  clearTimeout(timer);
671
- clearTaskCompleteTimer();
672
658
  if (exitFallbackTimer) { clearTimeout(exitFallbackTimer); exitFallbackTimer = null; }
673
659
  for (const f of cleanupFiles) safeUnlink(f);
674
660
  const parsed = acc.finalize();
@@ -706,7 +692,6 @@ function callLLM(promptText, sysPromptText, opts = {}) {
706
692
  if (resolved) return;
707
693
  resolved = true;
708
694
  clearTimeout(timer);
709
- clearTaskCompleteTimer();
710
695
  if (exitFallbackTimer) { clearTimeout(exitFallbackTimer); exitFallbackTimer = null; }
711
696
  for (const f of cleanupFiles) safeUnlink(f);
712
697
  shared.log('error', `LLM spawn error (${label}): ${err.message}`);
@@ -753,17 +738,6 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
753
738
  maxBudget, bare, fallbackModel,
754
739
  ...runtimeFeatureOpts,
755
740
  });
756
- let taskCompleteTimer = null;
757
- const scheduleTaskCompleteClose = () => {
758
- if (taskCompleteTimer) return;
759
- taskCompleteTimer = setTimeout(() => { try { shared.killImmediate(proc); } catch {} }, COPILOT_TASK_COMPLETE_GRACE_MS);
760
- };
761
- const clearTaskCompleteTimer = () => {
762
- if (taskCompleteTimer) {
763
- clearTimeout(taskCompleteTimer);
764
- taskCompleteTimer = null;
765
- }
766
- };
767
741
  let resolved = false;
768
742
  let exitFallbackTimer = null;
769
743
  let exitCode = null;
@@ -778,7 +752,6 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
778
752
  maxLineBufferBytes: ENGINE_DEFAULTS.maxLlmLineBufferBytes,
779
753
  onChunk,
780
754
  onToolUse,
781
- onTaskComplete: scheduleTaskCompleteClose,
782
755
  // Terminal text from the runtime adapter signals the LLM has logically
783
756
  // completed — kick the drain timer so we don't block on a delayed
784
757
  // 'exit'/'close' when an inherited pipe keeps the parent's FDs open.
@@ -797,7 +770,6 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
797
770
  if (resolved) return;
798
771
  resolved = true;
799
772
  clearTimeout(timer);
800
- clearTaskCompleteTimer();
801
773
  if (exitFallbackTimer) { clearTimeout(exitFallbackTimer); exitFallbackTimer = null; }
802
774
  for (const f of cleanupFiles) safeUnlink(f);
803
775
  const parsed = acc.finalize();
@@ -832,7 +804,6 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
832
804
  if (resolved) return;
833
805
  resolved = true;
834
806
  clearTimeout(timer);
835
- clearTaskCompleteTimer();
836
807
  if (exitFallbackTimer) { clearTimeout(exitFallbackTimer); exitFallbackTimer = null; }
837
808
  for (const f of cleanupFiles) safeUnlink(f);
838
809
  shared.log('error', `LLM-stream spawn error (${label}): ${err.message}`);
@@ -550,7 +550,7 @@ function parseOutput(raw, { maxTextLength = 0 } = {}) {
550
550
  const type = obj.type;
551
551
  if (type === 'assistant.message_delta') {
552
552
  const delta = obj.data?.deltaContent;
553
- if (typeof delta === 'string' && !taskCompleteSummary) pendingDeltaContent += delta;
553
+ if (typeof delta === 'string') pendingDeltaContent += delta;
554
554
  } else if (type === 'assistant.message') {
555
555
  const content = obj.data?.content;
556
556
  const toolRequests = obj.data?.toolRequests;
@@ -818,12 +818,13 @@ function createStreamConsumer(ctx) {
818
818
  // assistant messages or trailing deltas.
819
819
  let copilotMessageBuffer = '';
820
820
  let terminalText = '';
821
+ let taskCompleteSummary = '';
821
822
 
822
- function _captureTaskComplete(summary, success = true) {
823
+ function _captureTaskComplete(summary, success = true, { clearBuffer = false } = {}) {
823
824
  if (typeof summary !== 'string' || !summary) return;
824
- copilotMessageBuffer = '';
825
- if (!terminalText) {
826
- terminalText = summary;
825
+ taskCompleteSummary = summary;
826
+ if (clearBuffer) copilotMessageBuffer = '';
827
+ if (!terminalText && !copilotMessageBuffer) {
827
828
  ctx.pushText(summary);
828
829
  }
829
830
  ctx.notifyTaskComplete(summary, success !== false);
@@ -837,7 +838,8 @@ function createStreamConsumer(ctx) {
837
838
  // The result event is the first Copilot event that contains the resumable
838
839
  // sessionId. Do not mark the earlier assistant.message as terminal or
839
840
  // Minions can resolve before session persistence data is available.
840
- if (terminalText) ctx.setText(terminalText);
841
+ const finalText = terminalText || copilotMessageBuffer || taskCompleteSummary;
842
+ if (finalText) ctx.setText(finalText);
841
843
  }
842
844
 
843
845
  if (obj.type === 'session.task_complete') {
@@ -877,7 +879,7 @@ function createStreamConsumer(ctx) {
877
879
  for (const tr of data.toolRequests) {
878
880
  if (!tr || !tr.name) continue;
879
881
  if (tr.name === 'task_complete') {
880
- _captureTaskComplete(tr.arguments?.summary || tr.intentionSummary);
882
+ _captureTaskComplete(tr.arguments?.summary || tr.intentionSummary, true, { clearBuffer: true });
881
883
  continue;
882
884
  }
883
885
  ctx.pushToolUse(tr.name, tr.arguments || {});
@@ -888,7 +890,7 @@ function createStreamConsumer(ctx) {
888
890
 
889
891
  if (obj.type === 'tool.execution_start' && obj.data?.toolName) {
890
892
  if (obj.data.toolName === 'task_complete') {
891
- _captureTaskComplete(obj.data.arguments?.summary);
893
+ _captureTaskComplete(obj.data.arguments?.summary, true, { clearBuffer: true });
892
894
  return;
893
895
  }
894
896
  const name = obj.data.toolName;
@@ -903,6 +905,8 @@ function createStreamConsumer(ctx) {
903
905
 
904
906
  function reset() {
905
907
  copilotMessageBuffer = '';
908
+ terminalText = '';
909
+ taskCompleteSummary = '';
906
910
  }
907
911
 
908
912
  return { consume, reset };
package/engine.js CHANGED
@@ -407,6 +407,50 @@ function mergePendingSteeringEntries(...groups) {
407
407
  return merged;
408
408
  }
409
409
 
410
+ function promoteCheckpointSteeringForClose(agentId, procInfo, runtime, liveOutputPath) {
411
+ if (!procInfo || procInfo._steeringMessage) return { status: 'none', entries: [] };
412
+ if (runtime?.capabilities?.midRunSessionId !== false) return { status: 'none', entries: [] };
413
+
414
+ const startedAtMs = Date.parse(procInfo.startedAt);
415
+ const runStartMs = Number.isFinite(startedAtMs) ? startedAtMs : 0;
416
+ const pendingPaths = new Set((procInfo._pendingSteeringFiles || []).map(entry => entry?.path || entry).filter(Boolean));
417
+ const deferredPaths = new Set((procInfo._deferredSteeringFiles || []).filter(Boolean));
418
+ const unread = steering.listUnreadSteeringMessages(agentId).filter(entry => entry.message.trim());
419
+ const entriesByPath = new Map(unread.map(entry => [entry.path, entry]));
420
+ const pendingDeferred = Array.from(deferredPaths)
421
+ .map(filePath => entriesByPath.get(filePath))
422
+ .filter(Boolean);
423
+ const lateCheckpoint = unread.filter(entry =>
424
+ entry.createdAtMs >= runStartMs
425
+ && !pendingPaths.has(entry.path)
426
+ && !deferredPaths.has(entry.path)
427
+ );
428
+ const checkpointEntries = mergePendingSteeringEntries(pendingDeferred, lateCheckpoint);
429
+ if (checkpointEntries.length === 0) {
430
+ delete procInfo._deferredSteeringFiles;
431
+ return { status: 'none', entries: [] };
432
+ }
433
+
434
+ if (!procInfo.sessionId) {
435
+ log('warn', `Steering: ${agentId} exited before a resumable sessionId was available - ${checkpointEntries.length} message(s) remain pending`);
436
+ try { fs.appendFileSync(liveOutputPath, `\n[steering-pending] Agent exited before a resumable session was available. Your message remains unread and will be retried on the next dispatch.\n`); } catch {}
437
+ return { status: 'pending', entries: checkpointEntries };
438
+ }
439
+
440
+ if (pendingDeferred.length > 0) {
441
+ log('info', `Steering: delivering ${pendingDeferred.length} deferred message(s) for ${agentId} at resumable checkpoint`);
442
+ }
443
+ if (lateCheckpoint.length > 0) {
444
+ log('info', `Steering: delivering ${lateCheckpoint.length} late checkpoint message(s) for ${agentId} at resumable checkpoint`);
445
+ }
446
+ procInfo._steeringMessage = checkpointEntries.map(entry => entry.message.trim()).join('\n\n');
447
+ procInfo._steeringSessionId = procInfo.sessionId;
448
+ procInfo._steeringEntry = checkpointEntries;
449
+ procInfo._steeringDeferredCheckpoint = true;
450
+ delete procInfo._deferredSteeringFiles;
451
+ return { status: 'promoted', entries: checkpointEntries };
452
+ }
453
+
410
454
  // Resolve dependency plan item IDs to their PR branches
411
455
  function resolveDependencyBranches(depIds, sourcePlan, project, config) {
412
456
  const results = []; // [{ branch, prId }]
@@ -1193,6 +1237,7 @@ async function spawnAgent(dispatchItem, config) {
1193
1237
  const MAX_OUTPUT = 1024 * 1024; // 1MB
1194
1238
  let stdout = '';
1195
1239
  let stderr = '';
1240
+ let steeringAckStdout = '';
1196
1241
  const sessionCaptureState = { sessionLineBuffer: '' };
1197
1242
  let _trustCheckDone = false;
1198
1243
  const _spawnTime = Date.now();
@@ -1207,6 +1252,7 @@ async function spawnAgent(dispatchItem, config) {
1207
1252
  const chunk = data.toString();
1208
1253
  realActivityMap.set(id, Date.now());
1209
1254
  if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
1255
+ if (steeringAckStdout.length < MAX_OUTPUT) steeringAckStdout += chunk.slice(0, MAX_OUTPUT - steeringAckStdout.length);
1210
1256
  try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
1211
1257
 
1212
1258
  // Trust gate detection: check first 30s of output for trust/permission prompts
@@ -1254,26 +1300,8 @@ async function spawnAgent(dispatchItem, config) {
1254
1300
  }
1255
1301
 
1256
1302
  const procInfo = activeProcesses.get(id);
1257
- ackPendingSteeringFiles(agentId, procInfo, stdout);
1258
-
1259
- if (procInfo?._deferredSteeringFiles?.length && procInfo.sessionId) {
1260
- const deferredPaths = new Set(procInfo._deferredSteeringFiles);
1261
- const pendingDeferred = steering.listUnreadSteeringMessages(agentId)
1262
- .filter(entry => deferredPaths.has(entry.path) && entry.message.trim());
1263
- if (pendingDeferred.length > 0) {
1264
- log('info', `Steering: delivering ${pendingDeferred.length} deferred message(s) for ${agentId} at resumable checkpoint`);
1265
- procInfo._steeringMessage = pendingDeferred.map(entry => entry.message.trim()).join('\n\n');
1266
- procInfo._steeringSessionId = procInfo.sessionId;
1267
- procInfo._steeringEntry = pendingDeferred;
1268
- procInfo._steeringDeferredCheckpoint = true;
1269
- delete procInfo._deferredSteeringFiles;
1270
- } else {
1271
- delete procInfo._deferredSteeringFiles;
1272
- }
1273
- } else if (procInfo?._deferredSteeringFiles?.length) {
1274
- log('warn', `Steering: ${agentId} exited before a resumable sessionId was available — message remains pending`);
1275
- try { fs.appendFileSync(liveOutputPath, `\n[steering-pending] Agent exited before a resumable session was available. Your message remains unread and will be retried on the next dispatch.\n`); } catch {}
1276
- }
1303
+ ackPendingSteeringFiles(agentId, procInfo, steeringAckStdout);
1304
+ promoteCheckpointSteeringForClose(agentId, procInfo, runtime, liveOutputPath);
1277
1305
 
1278
1306
  // Check if this was a steering kill — re-spawn with resume
1279
1307
  if (procInfo?._steeringMessage) {
@@ -1401,6 +1429,7 @@ async function spawnAgent(dispatchItem, config) {
1401
1429
  ),
1402
1430
  });
1403
1431
 
1432
+ steeringAckStdout = '';
1404
1433
  // Live steering kills discard partial old output. Deferred checkpoint
1405
1434
  // steering keeps the completed turn output so completion parsing still
1406
1435
  // sees the original work if the follow-up only acknowledges steering.
@@ -1414,6 +1443,7 @@ async function spawnAgent(dispatchItem, config) {
1414
1443
  const chunk = data.toString();
1415
1444
  realActivityMap.set(id, Date.now());
1416
1445
  if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
1446
+ if (steeringAckStdout.length < MAX_OUTPUT) steeringAckStdout += chunk.slice(0, MAX_OUTPUT - steeringAckStdout.length);
1417
1447
  try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
1418
1448
  const resumeInfo = activeProcesses.get(id);
1419
1449
  markRuntimeResumeOutputSeen(resumeInfo);
@@ -4719,6 +4749,7 @@ module.exports = {
4719
4749
  parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
4720
4750
  isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, // exported for testing
4721
4751
  _maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
4752
+ promoteCheckpointSteeringForClose, // exported for testing
4722
4753
  normalizePrBranch, resolvePrBranch, prCausePart, getPrCauseHead, getPrCauseBase, getPrAutomationCauseKey, getPrAutomationDispatchKey, // exported for testing
4723
4754
 
4724
4755
  // Playbooks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1802",
3
+ "version": "0.1.1804",
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"