@yemi33/minions 0.1.2135 → 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.
- package/dashboard/js/live-stream.js +18 -0
- package/dashboard.js +41 -0
- package/engine/dispatch-events.js +88 -5
- package/engine.js +23 -0
- package/package.json +1 -1
|
@@ -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
|
|
60
|
+
function _readLinesFrom(file) {
|
|
52
61
|
try {
|
|
53
|
-
const raw = fs.readFileSync(
|
|
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
|
|
59
|
-
|
|
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 =
|
|
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.
|
|
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"
|