@yemi33/minions 0.1.812 → 0.1.814

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.
@@ -0,0 +1,589 @@
1
+ /**
2
+ * engine/teams.js — Microsoft Teams integration via Azure Bot Framework.
3
+ * Provides adapter creation, message posting, and conversation reference persistence.
4
+ * All functions are no-ops when Teams is disabled or botbuilder is not installed.
5
+ */
6
+
7
+ const path = require('path');
8
+ const shared = require('./shared');
9
+ const queries = require('./queries');
10
+
11
+ const { log, safeJson, mutateJsonFileLocked, ENGINE_DEFAULTS } = shared;
12
+ const { ENGINE_DIR, getConfig } = queries;
13
+ const cards = require('./teams-cards');
14
+
15
+ const TEAMS_STATE_PATH = path.join(ENGINE_DIR, 'teams-state.json');
16
+ const TEAMS_INBOX_PATH = path.join(ENGINE_DIR, 'teams-inbox.json');
17
+ const TEAMS_INBOX_CAP = 200;
18
+
19
+ // ── Rate Limiting & Circuit Breaker Constants ─────────────────────────────
20
+ const MAX_RETRIES_429 = 2;
21
+ const MAX_RETRIES_5XX = 3;
22
+ const CIRCUIT_FAILURE_THRESHOLD = 5;
23
+ const CIRCUIT_RECOVERY_MS = 10 * 60 * 1000; // 10 minutes
24
+ const OUTBOUND_QUEUE_MAX = 100;
25
+ const OUTBOUND_DRAIN_INTERVAL_MS = 250; // 4 messages per second
26
+
27
+ // Lazy-load botbuilder — may not be installed
28
+ let _botbuilder = null;
29
+ function getBotbuilder() {
30
+ if (_botbuilder) return _botbuilder;
31
+ try {
32
+ _botbuilder = require('botbuilder');
33
+ } catch {
34
+ log('warn', 'botbuilder package not installed — Teams integration unavailable');
35
+ _botbuilder = null;
36
+ }
37
+ return _botbuilder;
38
+ }
39
+
40
+ /**
41
+ * Merge user config.teams with ENGINE_DEFAULTS.teams.
42
+ * Returns the merged teams config object.
43
+ */
44
+ function getTeamsConfig() {
45
+ const config = getConfig();
46
+ const defaults = ENGINE_DEFAULTS.teams;
47
+ const user = config.teams || {};
48
+ return { ...defaults, ...user };
49
+ }
50
+
51
+ /**
52
+ * Returns true if Teams integration is enabled and has required credentials.
53
+ */
54
+ function isTeamsEnabled() {
55
+ const cfg = getTeamsConfig();
56
+ return cfg.enabled === true && !!cfg.appId && !!cfg.appPassword;
57
+ }
58
+
59
+ // Cached adapter instance — created once per process
60
+ let _adapter = null;
61
+
62
+ /**
63
+ * Create and return a BotFrameworkAdapter instance.
64
+ * Returns null when Teams is disabled or botbuilder is not installed.
65
+ */
66
+ function createAdapter() {
67
+ if (_adapter) return _adapter;
68
+
69
+ if (!isTeamsEnabled()) {
70
+ log('info', 'Teams adapter not created — integration disabled or missing credentials');
71
+ return null;
72
+ }
73
+
74
+ const botbuilder = getBotbuilder();
75
+ if (!botbuilder) return null;
76
+
77
+ const cfg = getTeamsConfig();
78
+ try {
79
+ _adapter = new botbuilder.CloudAdapter(
80
+ new botbuilder.ConfigurationBotFrameworkAuthentication({
81
+ MicrosoftAppId: cfg.appId,
82
+ MicrosoftAppPassword: cfg.appPassword,
83
+ MicrosoftAppType: 'SingleTenant',
84
+ })
85
+ );
86
+ log('info', 'Teams adapter created successfully');
87
+ return _adapter;
88
+ } catch (err) {
89
+ log('warn', `Teams adapter creation failed: ${err.message}`);
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Save a conversation reference for later proactive messaging.
96
+ * Uses mutateJsonFileLocked for concurrency safety.
97
+ * @param {string} key — identifier for this conversation (e.g. channel ID or user ID)
98
+ * @param {object} ref — conversation reference from TurnContext.getConversationReference()
99
+ */
100
+ function saveConversationRef(key, ref) {
101
+ if (!key || !ref) return;
102
+ mutateJsonFileLocked(TEAMS_STATE_PATH, (state) => {
103
+ if (!state.conversations) state.conversations = {};
104
+ state.conversations[key] = { ref, savedAt: shared.ts() };
105
+ });
106
+ log('info', `Teams conversation ref saved: ${key}`);
107
+ }
108
+
109
+ /**
110
+ * Retrieve a saved conversation reference.
111
+ * @param {string} key — identifier used in saveConversationRef
112
+ * @returns {object|null} — the conversation reference, or null if not found
113
+ */
114
+ function getConversationRef(key) {
115
+ if (!key) return null;
116
+ const state = safeJson(TEAMS_STATE_PATH) || {};
117
+ return state.conversations?.[key]?.ref || null;
118
+ }
119
+
120
+ // ── Circuit Breaker ───────────────────────────────────────────────────────
121
+
122
+ const _circuit = {
123
+ state: 'closed', // 'closed' | 'open' | 'half-open'
124
+ failures: 0,
125
+ openedAt: 0,
126
+ };
127
+
128
+ function _isCircuitOpen() {
129
+ if (_circuit.state === 'closed') return false;
130
+ if (_circuit.state === 'open') {
131
+ if (Date.now() - _circuit.openedAt >= CIRCUIT_RECOVERY_MS) {
132
+ _circuit.state = 'half-open';
133
+ log('info', 'Teams circuit breaker: half-open — allowing probe request');
134
+ return false;
135
+ }
136
+ return true;
137
+ }
138
+ return false; // half-open allows one probe
139
+ }
140
+
141
+ function _onSendSuccess() {
142
+ if (_circuit.state !== 'closed') {
143
+ log('info', `Teams circuit breaker: closed (was ${_circuit.state})`);
144
+ }
145
+ _circuit.state = 'closed';
146
+ _circuit.failures = 0;
147
+ }
148
+
149
+ function _onSendFailure() {
150
+ _circuit.failures++;
151
+ if (_circuit.state === 'half-open') {
152
+ _circuit.state = 'open';
153
+ _circuit.openedAt = Date.now();
154
+ log('warn', 'Teams circuit breaker: probe failed — reopening for 10 minutes');
155
+ } else if (_circuit.failures >= CIRCUIT_FAILURE_THRESHOLD) {
156
+ _circuit.state = 'open';
157
+ _circuit.openedAt = Date.now();
158
+ log('warn', `Teams circuit breaker: OPEN after ${_circuit.failures} consecutive failures — disabling for 10 minutes`);
159
+ }
160
+ }
161
+
162
+ // ── Retry Logic ───────────────────────────────────────────────────────────
163
+
164
+ function _sleep(ms) {
165
+ return new Promise(resolve => setTimeout(resolve, ms));
166
+ }
167
+
168
+ /**
169
+ * Execute sendFn with retry on 429 (Retry-After) and 5xx (exponential backoff).
170
+ * @param {Function} sendFn — async function that performs the actual send
171
+ */
172
+ async function _sendWithRetry(sendFn) {
173
+ let lastErr;
174
+ let retries429 = 0;
175
+ let retries5xx = 0;
176
+ const maxAttempts = 1 + MAX_RETRIES_429 + MAX_RETRIES_5XX;
177
+
178
+ for (let i = 0; i < maxAttempts; i++) {
179
+ try {
180
+ await sendFn();
181
+ return;
182
+ } catch (err) {
183
+ lastErr = err;
184
+ const status = err.statusCode || err.status || 0;
185
+
186
+ if (status === 429 && retries429 < MAX_RETRIES_429) {
187
+ retries429++;
188
+ const retryAfterSec = parseInt(err.headers?.['retry-after'] || '1', 10);
189
+ const delayMs = Math.max(retryAfterSec, 1) * 1000;
190
+ log('info', `Teams 429 — retry ${retries429}/${MAX_RETRIES_429} after ${delayMs}ms`);
191
+ await _sleep(delayMs);
192
+ continue;
193
+ }
194
+
195
+ if (status >= 500 && status < 600 && retries5xx < MAX_RETRIES_5XX) {
196
+ retries5xx++;
197
+ const backoffMs = Math.pow(2, retries5xx - 1) * 1000; // 1s, 2s, 4s
198
+ log('info', `Teams ${status} — retry ${retries5xx}/${MAX_RETRIES_5XX} after ${backoffMs}ms`);
199
+ await _sleep(backoffMs);
200
+ continue;
201
+ }
202
+
203
+ throw err;
204
+ }
205
+ }
206
+ if (lastErr) throw lastErr;
207
+ }
208
+
209
+ // ── Outbound Queue ────────────────────────────────────────────────────────
210
+
211
+ const _outboundQueue = [];
212
+ let _drainTimer = null;
213
+
214
+ function _enqueueMessage(key, content) {
215
+ if (_outboundQueue.length >= OUTBOUND_QUEUE_MAX) {
216
+ log('warn', `Teams outbound queue full (${OUTBOUND_QUEUE_MAX}) — dropping message for ${key}`);
217
+ return;
218
+ }
219
+ _outboundQueue.push({ key, content });
220
+ _startDrainTimer();
221
+ }
222
+
223
+ function _startDrainTimer() {
224
+ if (_drainTimer) return;
225
+ _drainTimer = setInterval(_drainQueue, OUTBOUND_DRAIN_INTERVAL_MS);
226
+ }
227
+
228
+ function _stopDrainTimer() {
229
+ if (_drainTimer) {
230
+ clearInterval(_drainTimer);
231
+ _drainTimer = null;
232
+ }
233
+ }
234
+
235
+ async function _drainQueue() {
236
+ if (_outboundQueue.length === 0) {
237
+ _stopDrainTimer();
238
+ return;
239
+ }
240
+
241
+ const item = _outboundQueue.shift();
242
+ if (!item) return;
243
+
244
+ try {
245
+ await _sendProactive(item.key, item.content);
246
+ } catch (err) {
247
+ log('warn', `Teams queued message failed for ${item.key}: ${err.message}`);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Send a proactive message with circuit breaker and retry.
253
+ * Called by the drain timer — not directly by callers.
254
+ */
255
+ async function _sendProactive(key, content) {
256
+ if (_isCircuitOpen()) {
257
+ log('info', `Teams circuit open — skipping message to ${key}`);
258
+ return;
259
+ }
260
+
261
+ const adapter = createAdapter();
262
+ if (!adapter) return;
263
+
264
+ const ref = getConversationRef(key);
265
+ if (!ref) {
266
+ log('warn', `Teams post skipped — no conversation ref for key: ${key}`);
267
+ return;
268
+ }
269
+
270
+ try {
271
+ const activity = toActivity(content);
272
+ await _sendWithRetry(async () => {
273
+ await adapter.continueConversationAsync(getTeamsConfig().appId, ref, async (context) => {
274
+ await context.sendActivity(activity);
275
+ });
276
+ });
277
+ _onSendSuccess();
278
+ const len = typeof content === 'string' ? content.length : JSON.stringify(content).length;
279
+ log('info', `Teams proactive message sent to ${key} (${len} chars)`);
280
+ } catch (err) {
281
+ _onSendFailure();
282
+ log('warn', `Teams proactive post failed for ${key}: ${err.message}`);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Build an activity object from text or Adaptive Card.
288
+ * @param {string|object} content — plain text string or Adaptive Card object (with type: 'AdaptiveCard')
289
+ * @returns {string|object} — activity-compatible value for sendActivity
290
+ */
291
+ function toActivity(content) {
292
+ if (typeof content === 'string') return content;
293
+ if (content && content.type === 'AdaptiveCard') {
294
+ return {
295
+ type: 'message',
296
+ text: content.fallbackText || '',
297
+ attachments: [{
298
+ contentType: 'application/vnd.microsoft.card.adaptive',
299
+ content,
300
+ }],
301
+ };
302
+ }
303
+ return String(content);
304
+ }
305
+
306
+ /**
307
+ * Reply to an existing Teams conversation turn.
308
+ * No-op when adapter is null (Teams disabled or botbuilder missing).
309
+ * @param {object} context — TurnContext from bot handler
310
+ * @param {string|object} content — message text or Adaptive Card object
311
+ */
312
+ async function teamsReply(context, content) {
313
+ const adapter = createAdapter();
314
+ if (!adapter || !context) return;
315
+ if (_isCircuitOpen()) {
316
+ log('info', 'Teams circuit open — skipping reply');
317
+ return;
318
+ }
319
+ try {
320
+ await _sendWithRetry(async () => {
321
+ await context.sendActivity(toActivity(content));
322
+ });
323
+ _onSendSuccess();
324
+ const len = typeof content === 'string' ? content.length : JSON.stringify(content).length;
325
+ log('info', `Teams reply sent (${len} chars)`);
326
+ } catch (err) {
327
+ _onSendFailure();
328
+ log('warn', `Teams reply failed: ${err.message}`);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Proactively post a message to a saved Teams conversation.
334
+ * No-op when adapter is null or conversation ref is not found.
335
+ * @param {string} key — conversation key used in saveConversationRef
336
+ * @param {string|object} content — message text or Adaptive Card object
337
+ */
338
+ async function teamsPost(key, content) {
339
+ if (!createAdapter()) return;
340
+ _enqueueMessage(key, content);
341
+ }
342
+
343
+ /**
344
+ * Process unread messages in the Teams inbox.
345
+ * Reads engine/teams-inbox.json, sends each unprocessed message through CC
346
+ * via the dashboard HTTP API, posts the CC response as a Teams reply,
347
+ * and marks the message as processed. Prunes oldest processed messages
348
+ * when inbox exceeds TEAMS_INBOX_CAP.
349
+ */
350
+ async function processTeamsInbox() {
351
+ if (!isTeamsEnabled()) return;
352
+
353
+ // Read inbox — snapshot unprocessed messages, then release lock
354
+ const inbox = safeJson(TEAMS_INBOX_PATH);
355
+ if (!Array.isArray(inbox) || inbox.length === 0) return;
356
+
357
+ const unprocessed = inbox.filter(m => !m._processedAt);
358
+ if (unprocessed.length === 0) return;
359
+
360
+ log('info', `Teams inbox: ${unprocessed.length} unprocessed message(s)`);
361
+ const cfg = getTeamsConfig();
362
+ const port = process.env.PORT || 7331;
363
+
364
+ // Process sequentially to avoid CC session conflicts
365
+ for (const msg of unprocessed) {
366
+ try {
367
+ // Call CC via dashboard HTTP API
368
+ const ccRes = await fetch(`http://localhost:${port}/api/command-center`, {
369
+ method: 'POST',
370
+ headers: { 'Content-Type': 'application/json' },
371
+ body: JSON.stringify({ message: msg.text, tabId: `teams-${msg.id}` }),
372
+ });
373
+ const ccData = await ccRes.json().catch(() => ({}));
374
+ const responseText = ccData.text || ccData.error || 'No response from Command Center';
375
+
376
+ // Track usage under 'teams' category
377
+ const llm = require('./llm');
378
+ llm.trackEngineUsage('teams', ccData.usage || null);
379
+
380
+ // Post reply to Teams
381
+ if (msg.conversationRef?.conversation?.id) {
382
+ await teamsPost(msg.conversationRef.conversation.id, responseText);
383
+ }
384
+
385
+ // Mark as processed
386
+ mutateJsonFileLocked(TEAMS_INBOX_PATH, (data) => {
387
+ if (!Array.isArray(data)) return data;
388
+ const entry = data.find(m => m.id === msg.id);
389
+ if (entry) entry._processedAt = new Date().toISOString();
390
+
391
+ // Prune oldest processed messages when inbox exceeds cap
392
+ if (data.length > TEAMS_INBOX_CAP) {
393
+ const processed = data.filter(m => m._processedAt).sort((a, b) => (a.receivedAt || '').localeCompare(b.receivedAt || ''));
394
+ const toRemove = data.length - TEAMS_INBOX_CAP;
395
+ const removeIds = new Set(processed.slice(0, toRemove).map(m => m.id));
396
+ return data.filter(m => !removeIds.has(m.id));
397
+ }
398
+ }, { defaultValue: [] });
399
+
400
+ log('info', `Teams inbox: processed message ${msg.id} from ${msg.from}`);
401
+ } catch (err) {
402
+ log('warn', `Teams inbox: failed to process message ${msg.id}: ${err.message}`);
403
+ }
404
+ }
405
+ }
406
+
407
+ // ── CC Mirror to Teams ─────────────────────────────────────────────────────
408
+
409
+ const CC_MIRROR_RATE_LIMIT_MS = 5000;
410
+ let _lastCCMirrorPost = 0;
411
+
412
+ /**
413
+ * Mirror a CC response to Teams so the team sees orchestration activity.
414
+ * Truncates response at 4000 chars with a dashboard link suffix.
415
+ * Rate-limited to 1 post per 5 seconds — excess posts silently skipped.
416
+ * @param {string} userMessage — the user's CC input
417
+ * @param {string} ccResponse — the CC response text
418
+ */
419
+ async function teamsPostCCResponse(userMessage, ccResponse) {
420
+ if (!isTeamsEnabled()) return;
421
+ const cfg = getTeamsConfig();
422
+ if (!cfg.ccMirror) return;
423
+
424
+ // Rate limit
425
+ const now = Date.now();
426
+ if (now - _lastCCMirrorPost < CC_MIRROR_RATE_LIMIT_MS) return;
427
+ _lastCCMirrorPost = now;
428
+
429
+ const card = cards.buildCCResponseCard(userMessage, ccResponse);
430
+
431
+ // Find first available conversation ref
432
+ const state = safeJson(TEAMS_STATE_PATH) || {};
433
+ const convKeys = Object.keys(state.conversations || {});
434
+ if (convKeys.length === 0) {
435
+ log('info', 'Teams CC mirror skipped — no conversation refs stored');
436
+ return;
437
+ }
438
+
439
+ await teamsPost(convKeys[0], card);
440
+ log('info', `Teams CC mirror sent`);
441
+ }
442
+
443
+ // ── Post-Completion Notifications ──────────────────────────────────────────
444
+
445
+ /**
446
+ * Notify Teams when an agent completes or fails a task.
447
+ * Only posts if the event type is in config.teams.notifyEvents.
448
+ * @param {object} dispatchItem — the dispatch entry (id, type, task, meta)
449
+ * @param {string} result — 'success', 'error', or 'timeout'
450
+ * @param {string} agentId — the agent that ran the task
451
+ */
452
+ async function teamsNotifyCompletion(dispatchItem, result, agentId) {
453
+ if (!isTeamsEnabled()) return;
454
+ const cfg = getTeamsConfig();
455
+ const eventType = result === 'success' ? 'agent-completed' : 'agent-failed';
456
+ if (!cfg.notifyEvents || !cfg.notifyEvents.includes(eventType)) return;
457
+
458
+ const title = dispatchItem.task || dispatchItem.meta?.item?.title || dispatchItem.id;
459
+ const prUrl = dispatchItem.meta?.pr?.url || dispatchItem.pr || '';
460
+ const item = { title, id: dispatchItem.meta?.item?.id || dispatchItem.id };
461
+ const card = cards.buildCompletionCard(agentId, item, result, prUrl || undefined);
462
+
463
+ // Find first available conversation ref
464
+ const state = safeJson(TEAMS_STATE_PATH) || {};
465
+ const convKeys = Object.keys(state.conversations || {});
466
+ if (convKeys.length === 0) return;
467
+
468
+ try {
469
+ await teamsPost(convKeys[0], card);
470
+ log('info', `Teams completion notification sent for ${dispatchItem.id} (${result})`);
471
+ } catch (err) {
472
+ log('warn', `Teams completion notification failed: ${err.message}`);
473
+ }
474
+ }
475
+
476
+ // ── PR Lifecycle Notifications ─────────────────────────────────────────────
477
+
478
+ /**
479
+ * Notify Teams about a PR lifecycle event (merge, abandon, build-failed, approved).
480
+ * Deduplicates via _teamsNotifiedEvents on PR object.
481
+ * @param {object} pr — the PR object from pull-requests.json
482
+ * @param {string} event — event type: 'pr-merged', 'pr-abandoned', 'build-failed', 'pr-approved'
483
+ * @param {object} project — the project config object
484
+ * @param {string} prFilePath — path to pull-requests.json for dedup write
485
+ */
486
+ async function teamsNotifyPrEvent(pr, event, project, prFilePath) {
487
+ if (!isTeamsEnabled()) return;
488
+ const cfg = getTeamsConfig();
489
+ if (!cfg.notifyEvents || !cfg.notifyEvents.includes(event)) return;
490
+
491
+ // Dedup check — don't re-notify the same event
492
+ if (pr._teamsNotifiedEvents && pr._teamsNotifiedEvents.includes(event)) return;
493
+
494
+ const card = cards.buildPrCard(pr, event, project);
495
+
496
+ // Find first available conversation ref
497
+ const state = safeJson(TEAMS_STATE_PATH) || {};
498
+ const convKeys = Object.keys(state.conversations || {});
499
+ if (convKeys.length === 0) return;
500
+
501
+ try {
502
+ await teamsPost(convKeys[0], card);
503
+ log('info', `Teams PR notification sent: ${event} for ${pr.id}`);
504
+
505
+ // Record dedup — update _teamsNotifiedEvents on PR via lock
506
+ if (prFilePath) {
507
+ mutateJsonFileLocked(prFilePath, (prs) => {
508
+ if (!Array.isArray(prs)) return prs;
509
+ const target = prs.find(p => p.id === pr.id);
510
+ if (target) {
511
+ if (!target._teamsNotifiedEvents) target._teamsNotifiedEvents = [];
512
+ if (!target._teamsNotifiedEvents.includes(event)) {
513
+ target._teamsNotifiedEvents.push(event);
514
+ }
515
+ }
516
+ }, { defaultValue: [] });
517
+ }
518
+ } catch (err) {
519
+ log('warn', `Teams PR notification failed for ${pr.id}: ${err.message}`);
520
+ }
521
+ }
522
+
523
+ // ── Plan Lifecycle Notifications ───────────────────────────────────────────
524
+
525
+ /**
526
+ * Notify Teams about a plan lifecycle event (completed, approved, rejected, verify-created).
527
+ * @param {object} planInfo — { name, file, project, doneCount, totalCount } or similar
528
+ * @param {string} event — 'plan-completed', 'plan-approved', 'plan-rejected', 'verify-created'
529
+ */
530
+ async function teamsNotifyPlanEvent(planInfo, event) {
531
+ if (!isTeamsEnabled()) return;
532
+ const cfg = getTeamsConfig();
533
+ if (!cfg.notifyEvents || !cfg.notifyEvents.includes(event)) return;
534
+
535
+ const planName = planInfo.name || planInfo.file || 'Unknown plan';
536
+ const card = cards.buildPlanCard(planInfo, event);
537
+
538
+ const state = safeJson(TEAMS_STATE_PATH) || {};
539
+ const convKeys = Object.keys(state.conversations || {});
540
+ if (convKeys.length === 0) return;
541
+
542
+ try {
543
+ await teamsPost(convKeys[0], card);
544
+ log('info', `Teams plan notification sent: ${event} for ${planName}`);
545
+ } catch (err) {
546
+ log('warn', `Teams plan notification failed: ${err.message}`);
547
+ }
548
+ }
549
+
550
+ // Reset cached adapter and internal state (for testing)
551
+ function _resetAdapter() {
552
+ _adapter = null;
553
+ _botbuilder = null;
554
+ _lastCCMirrorPost = 0;
555
+ _circuit.state = 'closed';
556
+ _circuit.failures = 0;
557
+ _circuit.openedAt = 0;
558
+ _outboundQueue.length = 0;
559
+ _stopDrainTimer();
560
+ }
561
+
562
+ module.exports = {
563
+ getTeamsConfig,
564
+ isTeamsEnabled,
565
+ createAdapter,
566
+ saveConversationRef,
567
+ getConversationRef,
568
+ teamsReply,
569
+ teamsPost,
570
+ processTeamsInbox,
571
+ teamsPostCCResponse,
572
+ teamsNotifyCompletion,
573
+ teamsNotifyPrEvent,
574
+ teamsNotifyPlanEvent,
575
+ CC_MIRROR_RATE_LIMIT_MS,
576
+ TEAMS_STATE_PATH,
577
+ TEAMS_INBOX_PATH,
578
+ TEAMS_INBOX_CAP,
579
+ MAX_RETRIES_429,
580
+ MAX_RETRIES_5XX,
581
+ CIRCUIT_FAILURE_THRESHOLD,
582
+ CIRCUIT_RECOVERY_MS,
583
+ OUTBOUND_QUEUE_MAX,
584
+ OUTBOUND_DRAIN_INTERVAL_MS,
585
+ _circuit, // exported for testing
586
+ _outboundQueue, // exported for testing
587
+ _resetAdapter, // exported for testing
588
+ _stopDrainTimer, // exported for testing
589
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.812",
3
+ "version": "0.1.814",
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"
@@ -41,7 +41,9 @@
41
41
  "devDependencies": {
42
42
  "@playwright/test": "^1.58.2"
43
43
  },
44
- "dependencies": {},
44
+ "dependencies": {
45
+ "botbuilder": "4.23.3"
46
+ },
45
47
  "publishConfig": {
46
48
  "access": "public"
47
49
  }