@yemi33/minions 0.1.2136 → 0.1.2138
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 +51 -0
- package/engine/consolidation.js +16 -1
- package/engine/dispatch-events.js +88 -5
- package/engine/shared.js +82 -0
- package/engine/watches.js +22 -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)
|
|
@@ -7429,6 +7470,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7429
7470
|
}
|
|
7430
7471
|
safeWrite(notesPath, notes);
|
|
7431
7472
|
|
|
7473
|
+
// W-mq1j85cj00055a8f — rewrite WI references that pointed at the
|
|
7474
|
+
// now-archived inbox note to the persisted destination (notes.md).
|
|
7475
|
+
try { shared.rewriteInboxRefsAcrossProjects(name, 'notes.md'); }
|
|
7476
|
+
catch (e) { console.error('inbox-ref rewrite (persist):', e.message); }
|
|
7477
|
+
|
|
7432
7478
|
// Move to archive
|
|
7433
7479
|
const archiveDir = path.join(MINIONS_DIR, 'notes', 'archive');
|
|
7434
7480
|
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
|
@@ -7468,6 +7514,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7468
7514
|
safeWrite(kbFile, kbContent);
|
|
7469
7515
|
queries.invalidateKnowledgeBaseCache();
|
|
7470
7516
|
|
|
7517
|
+
// W-mq1j85cj00055a8f — rewrite WI references that pointed at the
|
|
7518
|
+
// now-archived inbox note to the KB destination.
|
|
7519
|
+
try { shared.rewriteInboxRefsAcrossProjects(name, `knowledge/${category}/${name}`); }
|
|
7520
|
+
catch (e) { console.error('inbox-ref rewrite (promote-kb):', e.message); }
|
|
7521
|
+
|
|
7471
7522
|
// Move inbox item to archive
|
|
7472
7523
|
const archiveDir = path.join(MINIONS_DIR, 'notes', 'archive');
|
|
7473
7524
|
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
package/engine/consolidation.js
CHANGED
|
@@ -1155,7 +1155,22 @@ function archiveInboxFiles(files) {
|
|
|
1155
1155
|
|
|
1156
1156
|
if (!fs.existsSync(ARCHIVE_DIR)) fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
1157
1157
|
for (const f of files) {
|
|
1158
|
-
try {
|
|
1158
|
+
try {
|
|
1159
|
+
// Resolve the final destination path BEFORE rename so the WI-ref
|
|
1160
|
+
// rewrite can point at the actual on-disk location (uniquePath may
|
|
1161
|
+
// suffix `-2`, `-3` … if a same-day collision exists).
|
|
1162
|
+
const dest = shared.uniquePath(path.join(ARCHIVE_DIR, `${dateStamp()}-${f}`));
|
|
1163
|
+
// W-mq1j85cj00055a8f — rewrite WI references that pointed at the
|
|
1164
|
+
// now-archived inbox note to the archive destination. Computed
|
|
1165
|
+
// relative to MINIONS_DIR so it matches the dashboard's relative-URL
|
|
1166
|
+
// render convention. Wrapped so a rewrite failure can't break the
|
|
1167
|
+
// archive step itself (which is the load-bearing operation here).
|
|
1168
|
+
try {
|
|
1169
|
+
const rel = path.relative(shared.MINIONS_DIR, dest).replace(/\\/g, '/');
|
|
1170
|
+
shared.rewriteInboxRefsAcrossProjects(f, rel);
|
|
1171
|
+
} catch (e) { log('warn', `Inbox-ref rewrite (${f}): ${e.message}`); }
|
|
1172
|
+
fs.renameSync(path.join(INBOX_DIR, f), dest);
|
|
1173
|
+
} catch (err) { log('warn', `Inbox archive: ${err.message}`); }
|
|
1159
1174
|
}
|
|
1160
1175
|
}
|
|
1161
1176
|
|
|
@@ -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/shared.js
CHANGED
|
@@ -5090,6 +5090,87 @@ function extractStructuredWorkItemPrRef(item) {
|
|
|
5090
5090
|
return null;
|
|
5091
5091
|
}
|
|
5092
5092
|
|
|
5093
|
+
// W-mq1j85cj00055a8f — when an inbox note (notes/inbox/<name>) leaves the
|
|
5094
|
+
// inbox (persisted to notes.md, promoted to knowledge/, or auto-archived by
|
|
5095
|
+
// consolidation), any work-item reference pointing at the old inbox path
|
|
5096
|
+
// turns into a broken link. Rewrite those references across every project's
|
|
5097
|
+
// work-items.json + the central one to the new canonical location.
|
|
5098
|
+
//
|
|
5099
|
+
// Match semantics (intentionally narrow per the WI scope):
|
|
5100
|
+
// - Scope is strictly item.references[]; description prose is NOT scanned.
|
|
5101
|
+
// - String entries: 'notes/inbox/<inboxName>' (anchored on start or '/').
|
|
5102
|
+
// - Object entries: { url | path | href }. Other keys (label, kind, …) are
|
|
5103
|
+
// preserved.
|
|
5104
|
+
// - Trailing `?query` or `#fragment` is tolerated; substring overlaps in
|
|
5105
|
+
// unrelated path segments (e.g. .../notes/inbox/foobar for foo.md) are
|
|
5106
|
+
// NOT rewritten.
|
|
5107
|
+
// - Archived work-items files are skipped — only live work-items.json paths
|
|
5108
|
+
// surfaced by getProjects() + the central path are touched.
|
|
5109
|
+
//
|
|
5110
|
+
// Idempotent: re-running with the same inboxName after the rewrite is a
|
|
5111
|
+
// no-op (the references already read newLocation, which won't match the
|
|
5112
|
+
// notes/inbox/<inboxName> regex).
|
|
5113
|
+
//
|
|
5114
|
+
// Returns the count of references rewritten across all files (for logging).
|
|
5115
|
+
// Each file's mutate is wrapped in try/catch so one corrupt project can't
|
|
5116
|
+
// block the others.
|
|
5117
|
+
//
|
|
5118
|
+
// `opts._mutate` is an undocumented test seam — production callers always
|
|
5119
|
+
// take the default (the in-file `mutateWorkItems` closure reference). Tests
|
|
5120
|
+
// inject a wrapper to exercise the per-file try/catch boundary without
|
|
5121
|
+
// having to manufacture a real SQL-store failure.
|
|
5122
|
+
function rewriteInboxRefsAcrossProjects(inboxName, newLocation, opts = {}) {
|
|
5123
|
+
if (!inboxName || typeof newLocation !== 'string' || !newLocation) return 0;
|
|
5124
|
+
const baseName = String(inboxName).replace(/^.*[/\\]/, '').trim();
|
|
5125
|
+
if (!baseName) return 0;
|
|
5126
|
+
const escaped = baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
5127
|
+
const inboxRefRe = new RegExp(`(?:^|/)notes/inbox/${escaped}(?=$|[?#])`);
|
|
5128
|
+
const _mutate = (opts && typeof opts._mutate === 'function') ? opts._mutate : mutateWorkItems;
|
|
5129
|
+
|
|
5130
|
+
let rewritten = 0;
|
|
5131
|
+
const config = safeJson(path.join(MINIONS_DIR, 'config.json')) || {};
|
|
5132
|
+
const candidates = [];
|
|
5133
|
+
try {
|
|
5134
|
+
for (const project of getProjects(config)) {
|
|
5135
|
+
try { candidates.push(projectWorkItemsPath(project)); } catch { /* skip bad project entry */ }
|
|
5136
|
+
}
|
|
5137
|
+
} catch { /* getProjects failed — fall back to central only */ }
|
|
5138
|
+
candidates.push(centralWorkItemsPath());
|
|
5139
|
+
|
|
5140
|
+
for (const wiPath of candidates) {
|
|
5141
|
+
try {
|
|
5142
|
+
_mutate(wiPath, items => {
|
|
5143
|
+
if (!Array.isArray(items)) return items;
|
|
5144
|
+
for (const item of items) {
|
|
5145
|
+
if (!item || !Array.isArray(item.references)) continue;
|
|
5146
|
+
for (let i = 0; i < item.references.length; i++) {
|
|
5147
|
+
const ref = item.references[i];
|
|
5148
|
+
if (typeof ref === 'string') {
|
|
5149
|
+
if (inboxRefRe.test(ref)) {
|
|
5150
|
+
item.references[i] = newLocation;
|
|
5151
|
+
rewritten++;
|
|
5152
|
+
}
|
|
5153
|
+
} else if (ref && typeof ref === 'object') {
|
|
5154
|
+
for (const key of ['url', 'path', 'href']) {
|
|
5155
|
+
const v = ref[key];
|
|
5156
|
+
if (typeof v === 'string' && inboxRefRe.test(v)) {
|
|
5157
|
+
ref[key] = newLocation;
|
|
5158
|
+
rewritten++;
|
|
5159
|
+
break;
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
return items;
|
|
5166
|
+
});
|
|
5167
|
+
} catch (e) {
|
|
5168
|
+
try { console.warn(`rewriteInboxRefsAcrossProjects(${wiPath}): ${e.message}`); } catch { /* logging best-effort */ }
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
return rewritten;
|
|
5172
|
+
}
|
|
5173
|
+
|
|
5093
5174
|
function extractWorkItemPrRef(item) {
|
|
5094
5175
|
if (!item || typeof item !== 'object') return null;
|
|
5095
5176
|
const fromStructured = extractStructuredWorkItemPrRef(item);
|
|
@@ -6500,6 +6581,7 @@ module.exports = {
|
|
|
6500
6581
|
extractPrRefFromText,
|
|
6501
6582
|
extractWorkItemPrRef,
|
|
6502
6583
|
extractStructuredWorkItemPrRef,
|
|
6584
|
+
rewriteInboxRefsAcrossProjects,
|
|
6503
6585
|
getProjectPrScope,
|
|
6504
6586
|
getPrNumber,
|
|
6505
6587
|
getPrDisplayId,
|
package/engine/watches.js
CHANGED
|
@@ -320,7 +320,10 @@ function evaluateWatch(watch, state) {
|
|
|
320
320
|
if (!tt.conditions.includes(condition)) return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
321
321
|
|
|
322
322
|
const entity = tt.fetchEntity(target, state || {});
|
|
323
|
-
if (!entity)
|
|
323
|
+
if (!entity) {
|
|
324
|
+
const targetStr = typeof target === 'string' ? target : JSON.stringify(target);
|
|
325
|
+
return { triggered: false, message: `${tt.label} ${targetStr} not found` };
|
|
326
|
+
}
|
|
324
327
|
|
|
325
328
|
const prevState = watch._lastState || {};
|
|
326
329
|
let primary;
|
|
@@ -645,21 +648,35 @@ async function _runActionTask(task) {
|
|
|
645
648
|
/**
|
|
646
649
|
* Internal: capture state snapshot for a watch target.
|
|
647
650
|
* Dispatches to the registered target type's captureState.
|
|
651
|
+
*
|
|
652
|
+
* W-mq1n83pw000844b8 — Preserve prior state on transient fetchEntity null
|
|
653
|
+
* (and on captureState exceptions / unknown target types). The old behavior
|
|
654
|
+
* wiped to {} whenever the entity could not be resolved, which then made
|
|
655
|
+
* line 431 re-initialize via captureState on the next tick, losing
|
|
656
|
+
* type-specific dedup keys (gh-author-prs `numbers`, work-item
|
|
657
|
+
* `_unchangedTicks`, pipeline `_stuckStageTicks`). The result was watches
|
|
658
|
+
* re-firing for already-seen entities after every engine restart while a
|
|
659
|
+
* plugin's background fetch cache warmed up. Preserving prevState is
|
|
660
|
+
* harmless for types where fetchEntity-null means "entity deleted" because
|
|
661
|
+
* evaluate already gates on entity being non-null, and is the bug-free
|
|
662
|
+
* behavior for plugins with transient nulls (cache miss, network hiccup).
|
|
648
663
|
*/
|
|
649
664
|
function _captureState(watch, state) {
|
|
665
|
+
const prevState = watch._lastState || {};
|
|
650
666
|
const tt = TARGET_TYPES[watch.targetType];
|
|
651
|
-
if (!tt) return
|
|
667
|
+
if (!tt) return prevState;
|
|
652
668
|
const entity = tt.fetchEntity(watch.target, state || {});
|
|
653
|
-
if (!entity) return
|
|
669
|
+
if (!entity) return prevState;
|
|
654
670
|
try {
|
|
655
671
|
// P-w5b8d2c9 — Phase 2.2: pass prevState so captureState can carry
|
|
656
672
|
// forward unchanged-tick counters (e.g. _unchangedTicks for work-item
|
|
657
673
|
// stalled, _stuckStageTicks for pipeline stuck-in-stage). Existing
|
|
658
674
|
// captureState fns that take only 1 arg ignore this — backward-compat.
|
|
659
|
-
|
|
675
|
+
const out = tt.captureState(entity, prevState);
|
|
676
|
+
return (out && typeof out === 'object') ? out : prevState;
|
|
660
677
|
} catch (err) {
|
|
661
678
|
log('warn', `_captureState ${watch.targetType}: ${err.message}`);
|
|
662
|
-
return
|
|
679
|
+
return prevState;
|
|
663
680
|
}
|
|
664
681
|
}
|
|
665
682
|
|
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.2138",
|
|
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"
|