@yemi33/minions 0.1.2136 → 0.1.2137

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.
@@ -124,6 +124,24 @@ function startTerminalEventSource(agentId) {
124
124
  try { payload = JSON.parse(ev.data); } catch { return; }
125
125
  renderTerminalBannerForAgent(agentId, payload);
126
126
  });
127
+ // W-mq1m1wo5000nd78d — when the same agent starts a new dispatch, clear
128
+ // the prior dispatch's terminal banner. Without this, the stale banner
129
+ // from the previous dispatch lingers across same-agent re-dispatch
130
+ // because startLiveStream() only clears on tab switch / agent change.
131
+ liveTerminalSource.addEventListener('dispatch.started', function(ev) {
132
+ let payload;
133
+ try { payload = JSON.parse(ev.data); } catch { return; }
134
+ // Mirror the same guards used by renderTerminalBannerForAgent —
135
+ // a late-arriving started event for a stale agent / non-live tab
136
+ // must not wipe the current banner.
137
+ if (typeof currentAgentId !== 'undefined' && currentAgentId !== agentId) return;
138
+ if (typeof currentTab !== 'undefined' && currentTab !== 'live') return;
139
+ // Server-side guard already filters by agentId, but defend in depth
140
+ // for replay paths or future per-stream routing changes.
141
+ if (payload && payload.agentId && payload.agentId !== agentId) return;
142
+ const bannerEl = document.getElementById('live-terminal-banner');
143
+ if (bannerEl) bannerEl.innerHTML = '';
144
+ });
127
145
  // Ignore default `data:` log frames — log content arrives via polling.
128
146
  liveTerminalSource.onerror = function() {
129
147
  // EventSource auto-reconnects; nothing to do.
package/dashboard.js CHANGED
@@ -6092,6 +6092,46 @@ const server = http.createServer(async (req, res) => {
6092
6092
  };
6093
6093
  fs.watchFile(eventsFile, { interval: 500 }, terminalWatcher);
6094
6094
 
6095
+ // W-mq1m1wo5000nd78d — parallel watcher for `dispatch.started` events.
6096
+ // Symmetric with the terminal watcher above; the dashboard re-broadcasts
6097
+ // each new entry as an SSE `dispatch.started` frame. The live-view
6098
+ // client uses this to clear a stale terminal banner when the same agent
6099
+ // immediately starts a new dispatch. NO replay-on-connect: the
6100
+ // banner-clear is the only consumer and replaying a stale started event
6101
+ // after a viewer connects post-terminal would wipe the terminal banner.
6102
+ const _sentStartedDispatchIds = new Set();
6103
+ const sendStartedEvent = (event) => {
6104
+ if (!event || !event.dispatchId) return;
6105
+ if (_sentStartedDispatchIds.has(event.dispatchId)) return;
6106
+ _sentStartedDispatchIds.add(event.dispatchId);
6107
+ safeWrite(`event: dispatch.started\ndata: ${JSON.stringify(event)}\n\n`);
6108
+ };
6109
+ const startedEventsFile = dispatchEvents._startedEventsFile();
6110
+ let _startedEventsOffset = 0;
6111
+ try { _startedEventsOffset = fs.statSync(startedEventsFile).size; }
6112
+ catch { _startedEventsOffset = 0; }
6113
+ const startedWatcher = () => {
6114
+ if (_cleanedUp) return;
6115
+ let stat;
6116
+ try { stat = fs.statSync(startedEventsFile); } catch { return; }
6117
+ if (stat.size === _startedEventsOffset) return;
6118
+ if (stat.size < _startedEventsOffset) _startedEventsOffset = 0;
6119
+ try {
6120
+ const fd = fs.openSync(startedEventsFile, 'r');
6121
+ const buf = Buffer.alloc(stat.size - _startedEventsOffset);
6122
+ fs.readSync(fd, buf, 0, buf.length, _startedEventsOffset);
6123
+ fs.closeSync(fd);
6124
+ _startedEventsOffset = stat.size;
6125
+ const lines = buf.toString('utf8').split('\n').filter(Boolean);
6126
+ for (const line of lines) {
6127
+ let evt;
6128
+ try { evt = JSON.parse(line); } catch { continue; }
6129
+ if (evt && evt.agentId === agentId) sendStartedEvent(evt);
6130
+ }
6131
+ } catch { /* read race; next watcher tick retries */ }
6132
+ };
6133
+ fs.watchFile(startedEventsFile, { interval: 500 }, startedWatcher);
6134
+
6095
6135
  // Idempotent cleanup helper to prevent handle leaks
6096
6136
  const cleanup = () => {
6097
6137
  if (_cleanedUp) return;
@@ -6099,6 +6139,7 @@ const server = http.createServer(async (req, res) => {
6099
6139
  try { clearInterval(doneCheck); } catch { /* optional */ }
6100
6140
  try { fs.unwatchFile(liveLogPath, watcher); } catch { /* optional */ }
6101
6141
  try { fs.unwatchFile(eventsFile, terminalWatcher); } catch { /* optional */ }
6142
+ try { fs.unwatchFile(startedEventsFile, startedWatcher); } catch { /* optional */ }
6102
6143
  };
6103
6144
 
6104
6145
  // Check if agent is still active (poll every 5s)
@@ -46,21 +46,33 @@ function _eventsFile() {
46
46
  return path.join(shared.MINIONS_DIR, 'engine', 'dispatch-terminal-events.jsonl');
47
47
  }
48
48
 
49
+ function _startedEventsFile() {
50
+ // W-mq1m1wo5000nd78d — parallel ring for `dispatch.started` events. Kept
51
+ // separate from the terminal ring so a same-agent re-dispatch can clear
52
+ // the prior terminal banner without us having to peek/merge two event
53
+ // types in the dashboard tailer or worry about per-dispatch ordering
54
+ // when one ring rolls over before the other.
55
+ return path.join(shared.MINIONS_DIR, 'engine', 'dispatch-started-events.jsonl');
56
+ }
57
+
49
58
  function _now() { return Date.now(); }
50
59
 
51
- function _readLines() {
60
+ function _readLinesFrom(file) {
52
61
  try {
53
- const raw = fs.readFileSync(_eventsFile(), 'utf8');
62
+ const raw = fs.readFileSync(file, 'utf8');
54
63
  return raw.split('\n').filter(Boolean);
55
64
  } catch { return []; }
56
65
  }
57
66
 
58
- function _appendEventLine(event) {
59
- const file = _eventsFile();
67
+ function _readLines() {
68
+ return _readLinesFrom(_eventsFile());
69
+ }
70
+
71
+ function _appendEventLineTo(file, event) {
60
72
  const dir = path.dirname(file);
61
73
  try {
62
74
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
63
- let lines = _readLines();
75
+ let lines = _readLinesFrom(file);
64
76
  lines.push(JSON.stringify(event));
65
77
  // Drop entries older than REPLAY_TTL_MS, then cap by entry count.
66
78
  const cutoff = _now() - REPLAY_TTL_MS;
@@ -78,6 +90,10 @@ function _appendEventLine(event) {
78
90
  }
79
91
  }
80
92
 
93
+ function _appendEventLine(event) {
94
+ _appendEventLineTo(_eventsFile(), event);
95
+ }
96
+
81
97
  /**
82
98
  * Build + broadcast a dispatch terminal event. Called from completeDispatch()
83
99
  * after the dispatch record's `result` is committed. The event payload is the
@@ -163,6 +179,67 @@ function buildTerminalEvent({ dispatchId, agentId, result, reason, failureClass,
163
179
  };
164
180
  }
165
181
 
182
+ /**
183
+ * W-mq1m1wo5000nd78d — symmetric counterpart to `dispatch.terminal`. Called
184
+ * exactly once per FRESH spawn from engine.js#spawnAgent (NOT from cli.js
185
+ * re-attach paths, which inherit a still-live previous-dispatch banner that
186
+ * must not be clobbered by a synthetic restart-time started event).
187
+ *
188
+ * Sole consumer today: dashboard/js/live-stream.js — clears
189
+ * `#live-terminal-banner` so a stale "✓ Task complete" / "✗ Task ended"
190
+ * banner from the agent's prior dispatch doesn't linger across same-agent
191
+ * re-dispatch (e.g. Ripley finishing one scheduled run and immediately
192
+ * starting another).
193
+ *
194
+ * Shape:
195
+ * {
196
+ * type: 'dispatch.started',
197
+ * ts: <epoch-ms>,
198
+ * dispatchId: <string>,
199
+ * agentId: <string>,
200
+ * taskTitle: <string>,
201
+ * startedAt: <ISO string>,
202
+ * }
203
+ *
204
+ * No `replay-on-connect`: the banner-clear is the only consumer, and
205
+ * replaying a stale started event when a viewer opens AFTER terminal has
206
+ * already rendered would silently wipe the terminal banner. Live broadcast
207
+ * only — there's no stale banner to clear when the page just loaded.
208
+ */
209
+ function emitDispatchStarted(event) {
210
+ if (!event || typeof event !== 'object') return;
211
+ if (!event.type) event.type = 'dispatch.started';
212
+ if (!event.ts) event.ts = _now();
213
+ _appendEventLineTo(_startedEventsFile(), event);
214
+ try { _emitter.emit('dispatch.started', event); } catch { /* never throw */ }
215
+ }
216
+
217
+ function onDispatchStarted(listener) {
218
+ _emitter.on('dispatch.started', listener);
219
+ return () => _emitter.off('dispatch.started', listener);
220
+ }
221
+
222
+ function readRecentStartedEvents() {
223
+ const cutoff = _now() - REPLAY_TTL_MS;
224
+ return _readLinesFrom(_startedEventsFile())
225
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
226
+ .filter(e => e && (e.ts || 0) >= cutoff);
227
+ }
228
+
229
+ function buildStartedEvent({ dispatchId, agentId, taskTitle, startedAt }) {
230
+ let safeTitle = '';
231
+ if (typeof taskTitle === 'string' && taskTitle) {
232
+ safeTitle = taskTitle.split('\n')[0].slice(0, 240);
233
+ }
234
+ return {
235
+ type: 'dispatch.started',
236
+ dispatchId,
237
+ agentId: agentId || null,
238
+ taskTitle: safeTitle,
239
+ startedAt: startedAt || null,
240
+ };
241
+ }
242
+
166
243
  module.exports = {
167
244
  emitDispatchTerminal,
168
245
  onDispatchTerminal,
@@ -170,8 +247,14 @@ module.exports = {
170
247
  getLastTerminalForAgent,
171
248
  getLastTerminalForDispatch,
172
249
  buildTerminalEvent,
250
+ // dispatch.started (W-mq1m1wo5000nd78d)
251
+ emitDispatchStarted,
252
+ onDispatchStarted,
253
+ readRecentStartedEvents,
254
+ buildStartedEvent,
173
255
  // exposed for testing / dashboard tailer
174
256
  _eventsFile,
257
+ _startedEventsFile,
175
258
  MAX_RING_ENTRIES,
176
259
  REPLAY_TTL_MS,
177
260
  };
package/engine.js CHANGED
@@ -37,6 +37,7 @@ const { resolveRuntime } = require('./engine/runtimes');
37
37
  const { assertStaleHeadOk } = require('./engine/spawn-agent');
38
38
  const adoGitAuth = require('./engine/ado-git-auth');
39
39
  const queries = require('./engine/queries');
40
+ const dispatchEvents = require('./engine/dispatch-events');
40
41
 
41
42
  // ─── Paths ──────────────────────────────────────────────────────────────────
42
43
 
@@ -2684,6 +2685,28 @@ async function spawnAgent(dispatchItem, config) {
2684
2685
  };
2685
2686
  activeProcesses.set(id, initialProcInfo);
2686
2687
  registeredInActiveProcesses = true;
2688
+
2689
+ // W-mq1m1wo5000nd78d — emit dispatch.started symmetric with the
2690
+ // dispatch.terminal event emitted from dispatch.js#completeDispatch. The
2691
+ // dashboard's live-view client uses this to clear the prior dispatch's
2692
+ // terminal banner when the same agent immediately starts a new run
2693
+ // (otherwise the "✓ Task complete" / "✗ Task ended" banner from the
2694
+ // previous dispatch lingers because startLiveStream only clears the
2695
+ // banner on tab switch / agent change).
2696
+ //
2697
+ // ONLY emitted on fresh spawns — re-attach after engine restart goes
2698
+ // through engine/cli.js and does NOT pass through here, so a re-attached
2699
+ // dispatch (procInfo.reattached === true) won't synthesise a spurious
2700
+ // started event that could clobber a still-live previous-dispatch
2701
+ // banner that the user is mid-read on.
2702
+ try {
2703
+ dispatchEvents.emitDispatchStarted(dispatchEvents.buildStartedEvent({
2704
+ dispatchId: id,
2705
+ agentId,
2706
+ taskTitle: dispatchItem.task,
2707
+ startedAt,
2708
+ }));
2709
+ } catch (e) { log('warn', `emit dispatch.started: ${e.message}`); }
2687
2710
  } catch (spawnErr) {
2688
2711
  // Partial-setup cleanup (P-f4d2e8a1): tear down every artifact in the
2689
2712
  // reverse order it was created. Each step is conditional + best-effort
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2136",
3
+ "version": "0.1.2137",
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"