@yemi33/minions 0.1.1709 → 0.1.1711
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 +10 -0
- package/dashboard/js/command-center.js +3 -2
- package/dashboard.js +47 -2
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +8 -1
- package/engine/meeting.js +225 -77
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -969,7 +969,7 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
969
969
|
status.style.color = 'var(--green)';
|
|
970
970
|
}
|
|
971
971
|
ccAddMessage('action', status.outerHTML, false, targetTabId);
|
|
972
|
-
if (['dispatch','fix','implement','explore','review','test'].includes(action.type)) wakeEngine();
|
|
972
|
+
if (['dispatch','fix','implement','explore','review','test','create-meeting'].includes(action.type)) wakeEngine();
|
|
973
973
|
refresh();
|
|
974
974
|
return;
|
|
975
975
|
}
|
|
@@ -1175,9 +1175,10 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1175
1175
|
break;
|
|
1176
1176
|
}
|
|
1177
1177
|
case 'create-meeting': {
|
|
1178
|
+
var meetingParticipants = (Array.isArray(action.participants) && action.participants.length > 0) ? action.participants : (action.agents || []);
|
|
1178
1179
|
var res6 = await fetch('/api/meetings', {
|
|
1179
1180
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1180
|
-
body: JSON.stringify({ title: action.title, agenda: action.agenda, participants:
|
|
1181
|
+
body: JSON.stringify({ title: action.title, agenda: action.agenda, participants: meetingParticipants, rounds: action.rounds, project: action.project })
|
|
1181
1182
|
});
|
|
1182
1183
|
if (!res6.ok) { var d6 = await res6.json().catch(function() { return {}; }); throw new Error(d6.error || 'Meeting create failed'); }
|
|
1183
1184
|
var d6r = await res6.json();
|
package/dashboard.js
CHANGED
|
@@ -1539,6 +1539,27 @@ function _parseWatchInterval(val) {
|
|
|
1539
1539
|
return Math.max(60000, Math.round(u === 's' ? n * 1000 : u === 'm' ? n * 60000 : n * 3600000));
|
|
1540
1540
|
}
|
|
1541
1541
|
|
|
1542
|
+
function normalizeMeetingParticipants(participants) {
|
|
1543
|
+
if (!Array.isArray(participants)) return [];
|
|
1544
|
+
const seen = new Set();
|
|
1545
|
+
const normalized = [];
|
|
1546
|
+
for (const participant of participants) {
|
|
1547
|
+
const id = String(participant || '').trim();
|
|
1548
|
+
if (!id || seen.has(id)) continue;
|
|
1549
|
+
seen.add(id);
|
|
1550
|
+
normalized.push(id);
|
|
1551
|
+
}
|
|
1552
|
+
return normalized;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function meetingParticipantsFromAction(action) {
|
|
1556
|
+
return normalizeMeetingParticipants(
|
|
1557
|
+
Array.isArray(action?.participants) && action.participants.length > 0
|
|
1558
|
+
? action.participants
|
|
1559
|
+
: action?.agents
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1542
1563
|
// Required-field validator for CC actions. Returns null when valid, an error string when not.
|
|
1543
1564
|
// Centralises field-required checks so the model can't quietly emit a malformed action and have
|
|
1544
1565
|
// the server silently fall back to placeholder values (e.g. "Untitled"). The handler invokes this
|
|
@@ -1567,6 +1588,12 @@ function _ccValidateAction(action) {
|
|
|
1567
1588
|
case 'plan':
|
|
1568
1589
|
if (!action.title) return 'plan action missing required field: title';
|
|
1569
1590
|
return null;
|
|
1591
|
+
case 'create-meeting': {
|
|
1592
|
+
if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return 'create-meeting action missing required field: title';
|
|
1593
|
+
if (!action.agenda || typeof action.agenda !== 'string' || !action.agenda.trim()) return 'create-meeting action missing required field: agenda';
|
|
1594
|
+
if (meetingParticipantsFromAction(action).length < 2) return 'create-meeting action requires at least 2 participants';
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1570
1597
|
default:
|
|
1571
1598
|
return null; // unknown types fall through to existing handler / generic fallback
|
|
1572
1599
|
}
|
|
@@ -1794,6 +1821,17 @@ async function executeCCActions(actions) {
|
|
|
1794
1821
|
results.push({ type: 'create-watch', id: watch.id, ok: true });
|
|
1795
1822
|
break;
|
|
1796
1823
|
}
|
|
1824
|
+
case 'create-meeting': {
|
|
1825
|
+
const { createMeeting } = require('./engine/meeting');
|
|
1826
|
+
const meeting = createMeeting({
|
|
1827
|
+
title: action.title.trim(),
|
|
1828
|
+
agenda: action.agenda.trim(),
|
|
1829
|
+
participants: meetingParticipantsFromAction(action),
|
|
1830
|
+
});
|
|
1831
|
+
invalidateStatusCache();
|
|
1832
|
+
results.push({ type: 'create-meeting', id: meeting.id, ok: true });
|
|
1833
|
+
break;
|
|
1834
|
+
}
|
|
1797
1835
|
case 'delete-watch': {
|
|
1798
1836
|
const deleted = watchesMod.deleteWatch(action.id);
|
|
1799
1837
|
if (deleted) invalidateStatusCache();
|
|
@@ -6721,9 +6759,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6721
6759
|
{ method: 'POST', path: '/api/meetings', desc: 'Create a team meeting', params: 'title, agenda, participants[]', handler: async (req, res) => {
|
|
6722
6760
|
const body = await readBody(req);
|
|
6723
6761
|
const { title, agenda, participants } = body;
|
|
6724
|
-
if (
|
|
6762
|
+
if (typeof title !== 'string' || !title.trim() || typeof agenda !== 'string' || !agenda.trim()) {
|
|
6763
|
+
return jsonReply(res, 400, { error: 'title and agenda required' });
|
|
6764
|
+
}
|
|
6765
|
+
if (!Array.isArray(participants)) return jsonReply(res, 400, { error: 'participants must be an array' });
|
|
6766
|
+
const meetingParticipants = normalizeMeetingParticipants(participants);
|
|
6767
|
+
if (meetingParticipants.length < 2) return jsonReply(res, 400, { error: 'at least 2 participants required' });
|
|
6725
6768
|
const { createMeeting } = require('./engine/meeting');
|
|
6726
|
-
const meeting = createMeeting({ title, agenda, participants:
|
|
6769
|
+
const meeting = createMeeting({ title: title.trim(), agenda: agenda.trim(), participants: meetingParticipants });
|
|
6727
6770
|
invalidateStatusCache();
|
|
6728
6771
|
return jsonReply(res, 200, { ok: true, meeting });
|
|
6729
6772
|
}},
|
|
@@ -6923,6 +6966,8 @@ module.exports = {
|
|
|
6923
6966
|
_filterCcTabSessions,
|
|
6924
6967
|
_getVersionCheckInterval,
|
|
6925
6968
|
_parseWatchInterval,
|
|
6969
|
+
_normalizeMeetingParticipants: normalizeMeetingParticipants,
|
|
6970
|
+
_meetingParticipantsFromAction: meetingParticipantsFromAction,
|
|
6926
6971
|
parsePinnedEntries,
|
|
6927
6972
|
_parseDocChatResultText,
|
|
6928
6973
|
_messageRequestsOrchestration,
|
package/engine/lifecycle.js
CHANGED
|
@@ -2662,7 +2662,14 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2662
2662
|
if (type === WORK_TYPE.MEETING && meta?.meetingId) {
|
|
2663
2663
|
try {
|
|
2664
2664
|
const { collectMeetingFindings } = require('./meeting');
|
|
2665
|
-
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout, structuredCompletion
|
|
2665
|
+
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout, structuredCompletion, meta.round, {
|
|
2666
|
+
success: effectiveSuccess,
|
|
2667
|
+
result,
|
|
2668
|
+
code,
|
|
2669
|
+
completionStatus,
|
|
2670
|
+
agentReportedFailure,
|
|
2671
|
+
summary: resultSummary,
|
|
2672
|
+
});
|
|
2666
2673
|
} catch (err) { log('warn', `Meeting collect: ${err.message}`); }
|
|
2667
2674
|
}
|
|
2668
2675
|
|
package/engine/meeting.js
CHANGED
|
@@ -20,6 +20,106 @@ const EMPTY_OUTPUT_PATTERNS = ['(no output)', '(no findings)', '(no response)'];
|
|
|
20
20
|
// tests can redirect the meetings directory without patching module internals.
|
|
21
21
|
const MEETINGS_DIR = path.join(shared.MINIONS_DIR, 'meetings');
|
|
22
22
|
const MEETING_NOTE_ARTIFACT_ROOT = path.join(shared.MINIONS_DIR, 'notes', 'inbox');
|
|
23
|
+
const TERMINAL_MEETING_STATUSES = new Set(['completed', 'archived']);
|
|
24
|
+
const ROUND_STATUS_BY_NAME = {
|
|
25
|
+
investigate: 'investigating',
|
|
26
|
+
debate: 'debating',
|
|
27
|
+
conclude: 'concluding',
|
|
28
|
+
};
|
|
29
|
+
const ROUND_NUMBER_BY_NAME = { investigate: 1, debate: 2, conclude: 3 };
|
|
30
|
+
const ACTIVE_MEETING_STATUSES = new Set(Object.values(ROUND_STATUS_BY_NAME));
|
|
31
|
+
|
|
32
|
+
function isTerminalMeetingStatus(status) {
|
|
33
|
+
return TERMINAL_MEETING_STATUSES.has(String(status || '').toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function expectedMeetingStatusForRound(roundName) {
|
|
37
|
+
return ROUND_STATUS_BY_NAME[String(roundName || '').toLowerCase()] || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function roundKeyFor(roundName, round) {
|
|
41
|
+
const numeric = Number(round);
|
|
42
|
+
if (Number.isFinite(numeric) && numeric > 0) return String(numeric);
|
|
43
|
+
return String(ROUND_NUMBER_BY_NAME[String(roundName || '').toLowerCase()] || 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getRoundFailures(meeting, roundName, round, create = false) {
|
|
47
|
+
if (!meeting.roundFailures || typeof meeting.roundFailures !== 'object') {
|
|
48
|
+
if (!create) return {};
|
|
49
|
+
meeting.roundFailures = {};
|
|
50
|
+
}
|
|
51
|
+
const key = roundKeyFor(roundName, round);
|
|
52
|
+
if (!meeting.roundFailures[key] || typeof meeting.roundFailures[key] !== 'object') {
|
|
53
|
+
if (!create) return {};
|
|
54
|
+
meeting.roundFailures[key] = {};
|
|
55
|
+
}
|
|
56
|
+
return meeting.roundFailures[key];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasRoundFailure(meeting, roundName, agentId, round = meeting.round) {
|
|
60
|
+
return Boolean(getRoundFailures(meeting, roundName, round, false)[agentId]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasRoundSuccess(meeting, roundName, agentId) {
|
|
64
|
+
if (roundName === 'investigate') return Boolean(meeting.findings?.[agentId]);
|
|
65
|
+
if (roundName === 'debate') return Boolean(meeting.debate?.[agentId]);
|
|
66
|
+
return Boolean(meeting.conclusion && meeting.conclusion.agent === agentId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasRoundTerminalOutcome(meeting, roundName, agentId, round = meeting.round) {
|
|
70
|
+
return hasRoundSuccess(meeting, roundName, agentId) || hasRoundFailure(meeting, roundName, agentId, round);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function allParticipantsFinishedRound(meeting, roundName, round = meeting.round) {
|
|
74
|
+
const participants = Array.isArray(meeting.participants) ? meeting.participants : [];
|
|
75
|
+
return participants.length > 0 && participants.every(agentId =>
|
|
76
|
+
hasRoundTerminalOutcome(meeting, roundName, agentId, round)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatRoundFailuresForConclusion(meeting, roundName, round = meeting.round) {
|
|
81
|
+
const failures = getRoundFailures(meeting, roundName, round, false);
|
|
82
|
+
return Object.entries(failures)
|
|
83
|
+
.map(([agent, failure]) => `- **${agent}**: ${failure.reason || 'Agent failed before producing a meeting contribution.'}`)
|
|
84
|
+
.join('\n') || '- No structured failure details were captured.';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildFailedMeetingConclusion(meeting, agents, reason) {
|
|
88
|
+
const base = buildTimedOutMeetingConclusion(meeting, agents)
|
|
89
|
+
.replace('*Auto-generated — conclusion round timed out.*', '*Auto-generated — conclusion round failed.*');
|
|
90
|
+
return `${base}\n\n## Conclusion Failure\n${reason || formatRoundFailuresForConclusion(meeting, 'conclude', meeting.round)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function advanceMeetingIfRoundComplete(meeting, roundName, meetingId, config = null) {
|
|
94
|
+
if (roundName === 'investigate') {
|
|
95
|
+
if (!allParticipantsFinishedRound(meeting, roundName, meeting.round)) return false;
|
|
96
|
+
meeting.status = 'debating';
|
|
97
|
+
meeting.round = 2;
|
|
98
|
+
meeting.roundStartedAt = ts();
|
|
99
|
+
log('info', `Meeting ${meetingId}: all findings finished — advancing to debate`);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (roundName === 'debate') {
|
|
103
|
+
if (!allParticipantsFinishedRound(meeting, roundName, meeting.round)) return false;
|
|
104
|
+
meeting.status = 'concluding';
|
|
105
|
+
meeting.round = 3;
|
|
106
|
+
meeting.roundStartedAt = ts();
|
|
107
|
+
log('info', `Meeting ${meetingId}: all debate responses finished — advancing to conclusion`);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (roundName === 'conclude' && !meeting.conclusion) {
|
|
111
|
+
const agents = (config || queries.getConfig()).agents || {};
|
|
112
|
+
const autoConclusion = buildFailedMeetingConclusion(meeting, agents);
|
|
113
|
+
meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt: ts() };
|
|
114
|
+
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at: ts() });
|
|
115
|
+
meeting.status = 'completed';
|
|
116
|
+
meeting.completedAt = ts();
|
|
117
|
+
writeMeetingTranscriptToInbox(meeting, meetingId, agents);
|
|
118
|
+
log('warn', `Meeting ${meetingId}: conclusion failed — auto-generated fallback conclusion`);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
23
123
|
|
|
24
124
|
function isEmptyMeetingContent(text) {
|
|
25
125
|
const value = String(text || '').trim();
|
|
@@ -218,6 +318,15 @@ function buildTimedOutMeetingConclusion(meeting, agents) {
|
|
|
218
318
|
return `*Auto-generated — conclusion round timed out.*\n\nThis summary is based on ${findingsCount} finding${findingsCount === 1 ? '' : 's'} and ${debateCount} debate response${debateCount === 1 ? '' : 's'}.\n\n## Findings Highlights\n${findingsHighlights.join('\n')}\n\n## Debate Takeaways\n${(debateTakeaways.length ? debateTakeaways : fallbackDebate).join('\n')}\n\n## Recommended Next Steps\n${nextSteps.join('\n')}`;
|
|
219
319
|
}
|
|
220
320
|
|
|
321
|
+
function writeMeetingTranscriptToInbox(meeting, meetingId, agents) {
|
|
322
|
+
try {
|
|
323
|
+
const transcript = meeting.transcript.map(t =>
|
|
324
|
+
`### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
|
|
325
|
+
).join('\n\n---\n\n');
|
|
326
|
+
shared.writeToInbox('meeting', meetingId, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
|
|
327
|
+
} catch (e) { log('warn', `Meeting ${meetingId} inbox write: ${e.message}`); }
|
|
328
|
+
}
|
|
329
|
+
|
|
221
330
|
function getMeetings() {
|
|
222
331
|
if (!fs.existsSync(MEETINGS_DIR)) return [];
|
|
223
332
|
return fs.readdirSync(MEETINGS_DIR)
|
|
@@ -234,6 +343,8 @@ function getMeeting(id) {
|
|
|
234
343
|
if (!m.debate) m.debate = {};
|
|
235
344
|
if (!m.humanNotes) m.humanNotes = [];
|
|
236
345
|
if (!m.participants) m.participants = [];
|
|
346
|
+
if (!m.transcript) m.transcript = [];
|
|
347
|
+
if (!m.roundFailures || typeof m.roundFailures !== 'object') m.roundFailures = {};
|
|
237
348
|
}
|
|
238
349
|
return m;
|
|
239
350
|
}
|
|
@@ -257,6 +368,7 @@ function createMeeting({ title, agenda, participants }) {
|
|
|
257
368
|
debate: {},
|
|
258
369
|
conclusion: null,
|
|
259
370
|
humanNotes: [],
|
|
371
|
+
roundFailures: {},
|
|
260
372
|
transcript: [],
|
|
261
373
|
};
|
|
262
374
|
saveMeeting(meeting);
|
|
@@ -278,10 +390,11 @@ function discoverMeetingWork(config) {
|
|
|
278
390
|
);
|
|
279
391
|
|
|
280
392
|
for (const meeting of meetings) {
|
|
281
|
-
if (meeting.status
|
|
393
|
+
if (isTerminalMeetingStatus(meeting.status)) continue;
|
|
282
394
|
|
|
283
395
|
const round = meeting.round || 1;
|
|
284
396
|
const roundName = meeting.status; // investigating, debating, concluding
|
|
397
|
+
if (!ACTIVE_MEETING_STATUSES.has(roundName)) continue;
|
|
285
398
|
const agents = config.agents || {};
|
|
286
399
|
|
|
287
400
|
if (roundName === 'concluding') {
|
|
@@ -357,6 +470,8 @@ function discoverMeetingWork(config) {
|
|
|
357
470
|
// Skip if already submitted for this round
|
|
358
471
|
if (roundName === 'investigating' && meeting.findings?.[agentId]) continue;
|
|
359
472
|
if (roundName === 'debating' && meeting.debate?.[agentId]) continue;
|
|
473
|
+
const dispatchRoundName = roundName === 'investigating' ? 'investigate' : 'debate';
|
|
474
|
+
if (hasRoundFailure(meeting, dispatchRoundName, agentId, round)) continue;
|
|
360
475
|
|
|
361
476
|
const key = `meeting-${meeting.id}-r${round}-${agentId}`;
|
|
362
477
|
if (activeKeys.has(key)) continue;
|
|
@@ -401,7 +516,7 @@ function discoverMeetingWork(config) {
|
|
|
401
516
|
source: 'meeting',
|
|
402
517
|
meetingId: meeting.id,
|
|
403
518
|
round,
|
|
404
|
-
roundName:
|
|
519
|
+
roundName: dispatchRoundName,
|
|
405
520
|
}
|
|
406
521
|
});
|
|
407
522
|
}
|
|
@@ -413,20 +528,54 @@ function discoverMeetingWork(config) {
|
|
|
413
528
|
* Collect findings from a completed meeting agent.
|
|
414
529
|
* Called from runPostCompletionHooks when type === 'meeting'.
|
|
415
530
|
*/
|
|
416
|
-
function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null) {
|
|
531
|
+
function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null, expectedRound = null, completionInfo = {}) {
|
|
417
532
|
const meeting = getMeeting(meetingId);
|
|
418
533
|
if (!meeting) return;
|
|
419
|
-
if (meeting.status
|
|
534
|
+
if (isTerminalMeetingStatus(meeting.status)) {
|
|
420
535
|
log('info', `Ignoring late findings from ${agentId} for completed meeting ${meetingId}`);
|
|
421
536
|
return;
|
|
422
537
|
}
|
|
423
538
|
|
|
424
|
-
const
|
|
539
|
+
const expectedStatus = expectedMeetingStatusForRound(roundName);
|
|
540
|
+
if (!expectedStatus) {
|
|
541
|
+
log('warn', `Meeting ${meetingId}: ignoring ${agentId} output for unknown round "${roundName || '(empty)'}"`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (meeting.status !== expectedStatus) {
|
|
545
|
+
log('info', `Ignoring stale ${roundName} output from ${agentId} for meeting ${meetingId} currently ${meeting.status}`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (expectedRound !== null && expectedRound !== undefined && Number(meeting.round || 1) !== Number(expectedRound)) {
|
|
549
|
+
log('info', `Ignoring stale round ${expectedRound} output from ${agentId} for meeting ${meetingId} currently on round ${meeting.round || 1}`);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (hasRoundTerminalOutcome(meeting, roundName, agentId, meeting.round)) {
|
|
553
|
+
log('info', `Ignoring duplicate ${roundName} output from ${agentId} for meeting ${meetingId}`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
425
556
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
557
|
+
const content = resolveMeetingContributionContent(output, structuredCompletion);
|
|
558
|
+
const completionSucceeded = completionInfo?.success !== false;
|
|
559
|
+
|
|
560
|
+
if (!completionSucceeded || isEmptyMeetingContent(content)) {
|
|
561
|
+
const failures = getRoundFailures(meeting, roundName, meeting.round, true);
|
|
562
|
+
const reason = !completionSucceeded
|
|
563
|
+
? (completionInfo?.reason || completionInfo?.completionStatus || 'Agent failed before completing the meeting round')
|
|
564
|
+
: 'Agent produced empty meeting output';
|
|
565
|
+
failures[agentId] = {
|
|
566
|
+
reason,
|
|
567
|
+
content: content || completionInfo?.summary || '',
|
|
568
|
+
submittedAt: ts(),
|
|
569
|
+
};
|
|
570
|
+
meeting.transcript.push({
|
|
571
|
+
round: meeting.round,
|
|
572
|
+
agent: agentId,
|
|
573
|
+
type: 'failure',
|
|
574
|
+
content: reason,
|
|
575
|
+
at: ts(),
|
|
576
|
+
});
|
|
577
|
+
log('warn', `Meeting ${meetingId}: agent ${agentId} failed ${roundName} — ${reason}`);
|
|
578
|
+
advanceMeetingIfRoundComplete(meeting, roundName, meetingId);
|
|
430
579
|
saveMeeting(meeting);
|
|
431
580
|
return;
|
|
432
581
|
}
|
|
@@ -446,11 +595,7 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
|
|
|
446
595
|
// Write transcript to inbox so agents learn from it (slug-based dedup)
|
|
447
596
|
try {
|
|
448
597
|
const config = queries.getConfig();
|
|
449
|
-
|
|
450
|
-
const transcript = meeting.transcript.map(t =>
|
|
451
|
-
`### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
|
|
452
|
-
).join('\n\n---\n\n');
|
|
453
|
-
shared.writeToInbox('meeting', meetingId, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
|
|
598
|
+
writeMeetingTranscriptToInbox(meeting, meetingId, config.agents || {});
|
|
454
599
|
} catch (e) { log('warn', `Meeting ${meetingId} inbox write: ${e.message}`); }
|
|
455
600
|
|
|
456
601
|
log('info', `Meeting ${meetingId} completed — transcript written to inbox`);
|
|
@@ -458,26 +603,7 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
|
|
|
458
603
|
return;
|
|
459
604
|
}
|
|
460
605
|
|
|
461
|
-
|
|
462
|
-
const participantCount = meeting.participants.length;
|
|
463
|
-
const allSubmitted =
|
|
464
|
-
(meeting.status === 'investigating' && Object.keys(meeting.findings || {}).length >= participantCount) ||
|
|
465
|
-
(meeting.status === 'debating' && Object.keys(meeting.debate || {}).length >= participantCount);
|
|
466
|
-
|
|
467
|
-
if (allSubmitted) {
|
|
468
|
-
// Advance to next round
|
|
469
|
-
if (meeting.status === 'investigating') {
|
|
470
|
-
meeting.status = 'debating';
|
|
471
|
-
meeting.round = 2;
|
|
472
|
-
meeting.roundStartedAt = ts();
|
|
473
|
-
log('info', `Meeting ${meetingId}: all findings in — advancing to debate`);
|
|
474
|
-
} else if (meeting.status === 'debating') {
|
|
475
|
-
meeting.status = 'concluding';
|
|
476
|
-
meeting.round = 3;
|
|
477
|
-
meeting.roundStartedAt = ts();
|
|
478
|
-
log('info', `Meeting ${meetingId}: all debate responses in — advancing to conclusion`);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
606
|
+
advanceMeetingIfRoundComplete(meeting, roundName, meetingId);
|
|
481
607
|
|
|
482
608
|
saveMeeting(meeting);
|
|
483
609
|
}
|
|
@@ -494,21 +620,54 @@ function addMeetingNote(meetingId, note) {
|
|
|
494
620
|
function _killMeetingDispatches(meetingId) {
|
|
495
621
|
try {
|
|
496
622
|
const DISPATCH_PATH = path.join(shared.MINIONS_DIR, 'engine', 'dispatch.json');
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
// Remove from active and move to completed
|
|
623
|
+
const tmpDir = path.join(shared.MINIONS_DIR, 'engine', 'tmp');
|
|
624
|
+
const entriesToStop = [];
|
|
625
|
+
const filesToDelete = [];
|
|
501
626
|
shared.mutateJsonFileLocked(DISPATCH_PATH, (dp) => {
|
|
502
|
-
dp.
|
|
503
|
-
dp.
|
|
504
|
-
|
|
627
|
+
dp.pending = Array.isArray(dp.pending) ? dp.pending : [];
|
|
628
|
+
dp.active = Array.isArray(dp.active) ? dp.active : [];
|
|
629
|
+
dp.completed = Array.isArray(dp.completed) ? dp.completed : [];
|
|
630
|
+
|
|
631
|
+
for (const queue of ['pending', 'active']) {
|
|
632
|
+
const kept = [];
|
|
633
|
+
for (const d of dp[queue]) {
|
|
634
|
+
if (d.meta?.meetingId !== meetingId) {
|
|
635
|
+
kept.push(d);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
entriesToStop.push(d);
|
|
639
|
+
filesToDelete.push(path.join(tmpDir, `pid-${d.id}.pid`));
|
|
640
|
+
filesToDelete.push(path.join(tmpDir, `prompt-${d.id}.md`));
|
|
641
|
+
filesToDelete.push(path.join(tmpDir, `sysprompt-${d.id}.md`));
|
|
642
|
+
filesToDelete.push(path.join(tmpDir, `sysprompt-${d.id}.md.tmp`));
|
|
643
|
+
}
|
|
644
|
+
dp[queue] = kept;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
for (const d of entriesToStop) {
|
|
505
648
|
dp.completed.push({ ...d, result: DISPATCH_RESULT.ERROR, reason: 'Meeting ended/advanced by human', completed_at: ts() });
|
|
506
649
|
}
|
|
507
650
|
if (dp.completed.length > 100) dp.completed = dp.completed.slice(-100);
|
|
508
651
|
return dp;
|
|
509
652
|
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
510
|
-
|
|
511
|
-
|
|
653
|
+
|
|
654
|
+
const pidsToKill = [];
|
|
655
|
+
for (const d of entriesToStop) {
|
|
656
|
+
try {
|
|
657
|
+
const pidFile = path.join(tmpDir, `pid-${d.id}.pid`);
|
|
658
|
+
const pid = shared.validatePid(fs.readFileSync(pidFile, 'utf8').trim());
|
|
659
|
+
pidsToKill.push(pid);
|
|
660
|
+
} catch { /* pending entries and already-finished agents may not have PID files */ }
|
|
661
|
+
}
|
|
662
|
+
for (const pid of pidsToKill) {
|
|
663
|
+
try { shared.killGracefully({ pid }); } catch { /* process may already be dead */ }
|
|
664
|
+
}
|
|
665
|
+
for (const fp of filesToDelete) {
|
|
666
|
+
try { fs.unlinkSync(fp); } catch { /* sidecar may not exist */ }
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (entriesToStop.length > 0) log('info', `Killed ${entriesToStop.length} meeting dispatch(es) for ${meetingId}`);
|
|
670
|
+
return entriesToStop.length;
|
|
512
671
|
} catch (e) { log('warn', 'kill meeting dispatches: ' + e.message); return 0; }
|
|
513
672
|
}
|
|
514
673
|
|
|
@@ -563,7 +722,8 @@ function deleteMeeting(id) {
|
|
|
563
722
|
|
|
564
723
|
/**
|
|
565
724
|
* Check for meeting rounds that have exceeded the timeout.
|
|
566
|
-
*
|
|
725
|
+
* Timeout is observational: rounds advance only after every participant has
|
|
726
|
+
* succeeded or failed, and conclusion waits for the conclusion agent outcome.
|
|
567
727
|
* Called from engine.js tick cycle.
|
|
568
728
|
*/
|
|
569
729
|
function checkMeetingTimeouts(config) {
|
|
@@ -572,10 +732,13 @@ function checkMeetingTimeouts(config) {
|
|
|
572
732
|
|| ENGINE_DEFAULTS.meetingRoundTimeout;
|
|
573
733
|
|
|
574
734
|
for (const meeting of meetings) {
|
|
575
|
-
if (meeting.status
|
|
735
|
+
if (isTerminalMeetingStatus(meeting.status)) continue;
|
|
736
|
+
if (!ACTIVE_MEETING_STATUSES.has(meeting.status)) continue;
|
|
576
737
|
if (!meeting.roundStartedAt) continue;
|
|
577
738
|
|
|
578
|
-
const
|
|
739
|
+
const roundStartedMs = new Date(meeting.roundStartedAt).getTime();
|
|
740
|
+
if (!Number.isFinite(roundStartedMs)) continue;
|
|
741
|
+
const elapsed = Date.now() - roundStartedMs;
|
|
579
742
|
if (elapsed < timeout) continue;
|
|
580
743
|
|
|
581
744
|
const respondedCount = meeting.status === 'investigating'
|
|
@@ -585,38 +748,23 @@ function checkMeetingTimeouts(config) {
|
|
|
585
748
|
: 0;
|
|
586
749
|
const totalCount = meeting.participants.length;
|
|
587
750
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
751
|
+
const roundName = meeting.status === 'investigating'
|
|
752
|
+
? 'investigate'
|
|
753
|
+
: meeting.status === 'debating'
|
|
754
|
+
? 'debate'
|
|
755
|
+
: 'conclude';
|
|
756
|
+
|
|
757
|
+
if (roundName !== 'conclude') {
|
|
758
|
+
if (allParticipantsFinishedRound(meeting, roundName, meeting.round)) {
|
|
759
|
+
log('warn', `Meeting ${meeting.id}: round ${meeting.round} timed out after ${Math.round(elapsed / 60000)}min but all participants are terminal — advancing`);
|
|
760
|
+
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round ${meeting.round} timed out after all participants finished`, at: ts() });
|
|
761
|
+
advanceMeetingIfRoundComplete(meeting, roundName, meeting.id, config);
|
|
762
|
+
saveMeeting(meeting);
|
|
763
|
+
} else {
|
|
764
|
+
log('warn', `Meeting ${meeting.id}: round ${meeting.round} timed out after ${Math.round(elapsed / 60000)}min — waiting for all participants to finish (${respondedCount}/${totalCount} succeeded)`);
|
|
765
|
+
}
|
|
602
766
|
} else if (meeting.status === 'concluding') {
|
|
603
|
-
log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min —
|
|
604
|
-
const autoConclusion = buildTimedOutMeetingConclusion(meeting, config.agents || {});
|
|
605
|
-
meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt: ts() };
|
|
606
|
-
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at: ts() });
|
|
607
|
-
meeting.status = 'completed';
|
|
608
|
-
meeting.completedAt = ts();
|
|
609
|
-
|
|
610
|
-
// Write transcript to inbox (same as normal conclusion path)
|
|
611
|
-
try {
|
|
612
|
-
const agents = config.agents || {};
|
|
613
|
-
const transcript = meeting.transcript.map(t =>
|
|
614
|
-
`### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
|
|
615
|
-
).join('\n\n---\n\n');
|
|
616
|
-
shared.writeToInbox('meeting', meeting.id, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
|
|
617
|
-
} catch (e) { log('warn', `Meeting ${meeting.id} inbox write: ${e.message}`); }
|
|
618
|
-
|
|
619
|
-
saveMeeting(meeting);
|
|
767
|
+
log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min — waiting for the conclusion agent to finish`);
|
|
620
768
|
}
|
|
621
769
|
}
|
|
622
770
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1711",
|
|
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"
|