@yemi33/minions 0.1.1651 → 0.1.1652

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1652 (2026-05-01)
4
+
5
+ ### Other
6
+ - Harden agent steering reliability
7
+
3
8
  ## 0.1.1651 (2026-05-01)
4
9
 
5
10
  ### Other
@@ -136,12 +136,14 @@ async function sendSteering() {
136
136
  try {
137
137
  const liveRes = await fetch('/api/agent/' + encodeURIComponent(currentAgentId) + '/live-output');
138
138
  const text = await liveRes.text();
139
- // Check if there's new output after the [human-steering] line
139
+ // Check if there's runtime output after the [human-steering] line.
140
+ // Claude emits "assistant"; Copilot emits "assistant.message_delta"
141
+ // / "assistant.message", and either one proves the resume turn ran.
140
142
  const steerIdx = text.lastIndexOf('[human-steering]');
141
143
  if (steerIdx >= 0) {
142
- const afterSteer = text.slice(steerIdx + 100);
143
- // Look for assistant response (JSON with type:assistant or readable text)
144
- if (afterSteer.length > 200 && (afterSteer.includes('"type":"assistant"') || afterSteer.includes('"type":"text"'))) {
144
+ const afterSteer = text.slice(steerIdx + '[human-steering]'.length);
145
+ const sawRuntimeOutput = /"type"\s*:\s*"(assistant(?:\.|")|tool\.|session\.task_complete"|result")/.test(afterSteer);
146
+ if (afterSteer.length > 20 && sawRuntimeOutput) {
145
147
  clearInterval(ackInterval);
146
148
  pending.textContent = '\u2713 Agent acknowledged';
147
149
  pending.style.color = 'var(--green)';
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-01T02:26:00.431Z"
4
+ "cachedAt": "2026-05-01T02:33:34.697Z"
5
5
  }
@@ -162,6 +162,27 @@ function _processEvidenceTimes(rawOutput, observedAtMs) {
162
162
  return times;
163
163
  }
164
164
 
165
+ function sessionIdFromEvent(obj) {
166
+ if (!obj || typeof obj !== 'object') return null;
167
+ const candidates = [
168
+ obj.session_id,
169
+ obj.sessionId,
170
+ obj.data?.session_id,
171
+ obj.data?.sessionId,
172
+ ];
173
+ for (const value of candidates) {
174
+ if (typeof value === 'string' && value.trim()) return value.trim();
175
+ }
176
+ if (obj.raw && obj.raw !== obj) return sessionIdFromEvent(obj.raw);
177
+ return null;
178
+ }
179
+
180
+ function sessionIdFromOutputLine(line) {
181
+ const trimmed = String(line || '').trim();
182
+ if (!trimmed.startsWith('{')) return null;
183
+ try { return sessionIdFromEvent(JSON.parse(trimmed)); } catch { return null; }
184
+ }
185
+
165
186
  function ackProcessedSteeringMessages(agentId, pendingEntries, rawOutput, opts = {}) {
166
187
  const entries = Array.isArray(pendingEntries) ? pendingEntries : [];
167
188
  if (entries.length === 0) return [];
@@ -183,5 +204,7 @@ module.exports = {
183
204
  writeSteeringMessage,
184
205
  listUnreadSteeringMessages,
185
206
  buildPendingSteeringPrompt,
207
+ sessionIdFromEvent,
208
+ sessionIdFromOutputLine,
186
209
  ackProcessedSteeringMessages,
187
210
  };
package/engine.js CHANGED
@@ -283,6 +283,7 @@ function _classifyAgentFailure(runtime, code, stdout, stderr) {
283
283
 
284
284
  function ackPendingSteeringFiles(agentId, procInfo, rawOutput, observedAtMs = Date.now()) {
285
285
  if (!procInfo?._pendingSteeringFiles?.length || !rawOutput) return;
286
+ if (procInfo._steeringMessage || procInfo._steeringNoSession) return;
286
287
  const acked = steering.ackProcessedSteeringMessages(agentId, procInfo._pendingSteeringFiles, rawOutput, { observedAtMs });
287
288
  if (acked.length === 0) return;
288
289
 
@@ -292,6 +293,45 @@ function ackPendingSteeringFiles(agentId, procInfo, rawOutput, observedAtMs = Da
292
293
  log('info', `Steering: ACKed ${acked.length} processed message(s) for ${agentId}`);
293
294
  }
294
295
 
296
+ function captureSessionIdFromStdoutChunk(agentId, dispatchId, branchName, runtime, procInfo, chunk, state) {
297
+ if (!procInfo || procInfo.sessionId || !chunk) return;
298
+ const text = String(state.sessionLineBuffer || '') + String(chunk);
299
+ const lines = text.split('\n');
300
+ state.sessionLineBuffer = /[\r\n]$/.test(text) ? '' : (lines.pop() || '');
301
+ if (state.sessionLineBuffer.length > 65536) state.sessionLineBuffer = '';
302
+
303
+ for (const line of lines) {
304
+ const sessionId = steering.sessionIdFromOutputLine(line);
305
+ if (!sessionId) continue;
306
+ procInfo.sessionId = sessionId;
307
+ if (runtime && typeof runtime.saveSession === 'function') {
308
+ runtime.saveSession({
309
+ agentId,
310
+ dispatchId,
311
+ branch: branchName,
312
+ sessionId,
313
+ agentsDir: AGENTS_DIR,
314
+ logger: _runtimeLogger(),
315
+ });
316
+ }
317
+ return;
318
+ }
319
+ }
320
+
321
+ function mergePendingSteeringEntries(...groups) {
322
+ const merged = [];
323
+ const seen = new Set();
324
+ for (const group of groups) {
325
+ const entries = Array.isArray(group) ? group : (group ? [group] : []);
326
+ for (const entry of entries) {
327
+ if (!entry?.path || seen.has(entry.path)) continue;
328
+ seen.add(entry.path);
329
+ merged.push(entry);
330
+ }
331
+ }
332
+ return merged;
333
+ }
334
+
295
335
  // Resolve dependency plan item IDs to their PR branches
296
336
  function resolveDependencyBranches(depIds, sourcePlan, project, config) {
297
337
  const results = []; // [{ branch, prId }]
@@ -1005,6 +1045,7 @@ async function spawnAgent(dispatchItem, config) {
1005
1045
  const MAX_OUTPUT = 1024 * 1024; // 1MB
1006
1046
  let stdout = '';
1007
1047
  let stderr = '';
1048
+ const sessionCaptureState = { sessionLineBuffer: '' };
1008
1049
  let _trustCheckDone = false;
1009
1050
  const _spawnTime = Date.now();
1010
1051
 
@@ -1028,30 +1069,10 @@ async function spawnAgent(dispatchItem, config) {
1028
1069
  _trustCheckDone = true; // past 30s window
1029
1070
  }
1030
1071
 
1031
- // Capture sessionId early for mid-session steering
1072
+ // Capture sessionId early for mid-session steering. Claude emits session_id;
1073
+ // Copilot emits sessionId, so use the runtime-neutral steering helper.
1032
1074
  const procInfo = activeProcesses.get(id);
1033
- if (procInfo && !procInfo.sessionId && chunk.includes('session_id')) {
1034
- try {
1035
- for (const line of chunk.split('\n')) {
1036
- if (!line.trim() || !line.startsWith('{')) continue;
1037
- const obj = JSON.parse(line);
1038
- if (obj.session_id) {
1039
- procInfo.sessionId = obj.session_id;
1040
- if (runtime && typeof runtime.saveSession === 'function') {
1041
- runtime.saveSession({
1042
- agentId,
1043
- dispatchId: id,
1044
- branch: branchName,
1045
- sessionId: obj.session_id,
1046
- agentsDir: AGENTS_DIR,
1047
- logger: _runtimeLogger(),
1048
- });
1049
- }
1050
- break;
1051
- }
1052
- }
1053
- } catch { /* JSON parse — output may not be valid JSON */ }
1054
- }
1075
+ captureSessionIdFromStdoutChunk(agentId, id, branchName, runtime, procInfo, chunk, sessionCaptureState);
1055
1076
 
1056
1077
  ackPendingSteeringFiles(agentId, procInfo, chunk);
1057
1078
  });
@@ -1112,8 +1133,12 @@ async function spawnAgent(dispatchItem, config) {
1112
1133
  }
1113
1134
  }
1114
1135
 
1115
- // Write new prompt with steering message
1116
- const steerPrompt = `Message from your human teammate:\n\n${steerMsg}\n\nRespond to this, then continue working on your current task.`;
1136
+ // Write new prompt with all unACKed steering messages. This keeps delivery
1137
+ // durable if the killed process had older pending messages that never
1138
+ // produced processing evidence before the resume.
1139
+ const pendingForResume = steering.buildPendingSteeringPrompt(agentId);
1140
+ const steerPromptBody = pendingForResume.prompt || steerMsg;
1141
+ const steerPrompt = `Message from your human teammate:\n\n${steerPromptBody}\n\nRespond to this, then continue working on your current task.`;
1117
1142
  const steerPromptPath = path.join(ENGINE_DIR, 'tmp', `prompt-steer-${safeId}.md`);
1118
1143
  try { safeWrite(steerPromptPath, steerPrompt); } catch (e) {
1119
1144
  log('warn', `Steering: failed to write prompt for ${agentId}: ${e.message}`);
@@ -1180,19 +1205,26 @@ async function spawnAgent(dispatchItem, config) {
1180
1205
  startedAt: procInfo.startedAt,
1181
1206
  sessionId: steerSessionId,
1182
1207
  lastRealOutputAt: Date.now(),
1183
- _pendingSteeringFiles: steerEntry ? [steerEntry] : (procInfo._pendingSteeringFiles || []),
1208
+ _pendingSteeringFiles: mergePendingSteeringEntries(
1209
+ procInfo._pendingSteeringFiles,
1210
+ pendingForResume.entries,
1211
+ steerEntry,
1212
+ ),
1184
1213
  });
1185
1214
 
1186
1215
  // Reset output buffers so post-completion parsing only sees the resumed session
1187
1216
  stdout = '';
1188
1217
  stderr = '';
1218
+ sessionCaptureState.sessionLineBuffer = '';
1189
1219
  // Re-wire stdout/stderr handlers (same as original)
1190
1220
  resumeProc.stdout.on('data', (data) => {
1191
1221
  const chunk = data.toString();
1192
1222
  realActivityMap.set(id, Date.now());
1193
1223
  if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
1194
1224
  try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
1195
- ackPendingSteeringFiles(agentId, activeProcesses.get(id), chunk);
1225
+ const resumeInfo = activeProcesses.get(id);
1226
+ captureSessionIdFromStdoutChunk(agentId, id, branchName, runtime, resumeInfo, chunk, sessionCaptureState);
1227
+ ackPendingSteeringFiles(agentId, resumeInfo, chunk);
1196
1228
  });
1197
1229
  resumeProc.stderr.on('data', (data) => {
1198
1230
  const chunk = data.toString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1651",
3
+ "version": "0.1.1652",
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"