@agenticmail/enterprise 0.5.207 → 0.5.209

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.
Files changed (58) hide show
  1. package/dist/agent-heartbeat-2D43IBNA.js +510 -0
  2. package/dist/agent-heartbeat-AYOPPYNS.js +510 -0
  3. package/dist/agent-heartbeat-BLFZHGVH.js +510 -0
  4. package/dist/agent-heartbeat-FITCBARW.js +510 -0
  5. package/dist/chunk-2DJEQCRU.js +4457 -0
  6. package/dist/chunk-6PIWF5AC.js +1224 -0
  7. package/dist/chunk-APRK35OM.js +3685 -0
  8. package/dist/chunk-BTT3P4XP.js +3685 -0
  9. package/dist/chunk-D7SBTNBI.js +1224 -0
  10. package/dist/chunk-FEEHLIWF.js +1224 -0
  11. package/dist/chunk-JBLDCQSK.js +4457 -0
  12. package/dist/chunk-L72IFQCJ.js +1224 -0
  13. package/dist/chunk-LIHFQM2M.js +3685 -0
  14. package/dist/chunk-QVKIYQ5F.js +4494 -0
  15. package/dist/chunk-RTW7WNXZ.js +3685 -0
  16. package/dist/chunk-VBLS4HXF.js +4494 -0
  17. package/dist/chunk-W5MVEM4M.js +3685 -0
  18. package/dist/chunk-WZ4F4SYK.js +292 -0
  19. package/dist/chunk-ZJZ5N4SD.js +4494 -0
  20. package/dist/chunk-ZKSOJILT.js +1224 -0
  21. package/dist/cli-agent-ILMIH4Z4.js +1602 -0
  22. package/dist/cli-agent-MRGC2WFN.js +1602 -0
  23. package/dist/cli-agent-ND6P7N2G.js +1719 -0
  24. package/dist/cli-agent-OTZ6INTM.js +1602 -0
  25. package/dist/cli-agent-OXSEQWTQ.js +1602 -0
  26. package/dist/cli-serve-3M2FUSYX.js +114 -0
  27. package/dist/cli-serve-A4DRJBGH.js +114 -0
  28. package/dist/cli-serve-ASW2VFVZ.js +114 -0
  29. package/dist/cli-serve-MJSIZKOG.js +114 -0
  30. package/dist/cli-serve-V7JBBESA.js +114 -0
  31. package/dist/cli.js +3 -3
  32. package/dist/index.js +3 -3
  33. package/dist/routes-AAXSMKBT.js +13032 -0
  34. package/dist/routes-EZVM3AQW.js +13281 -0
  35. package/dist/routes-I7Z57PKK.js +13283 -0
  36. package/dist/routes-RGYOWZA6.js +13282 -0
  37. package/dist/runtime-CMOHWZTC.js +45 -0
  38. package/dist/runtime-DIE72WMW.js +45 -0
  39. package/dist/runtime-DVDGBQKF.js +45 -0
  40. package/dist/runtime-HWQ5ZXD3.js +45 -0
  41. package/dist/runtime-LRKG2KTN.js +45 -0
  42. package/dist/server-6L3RFC42.js +15 -0
  43. package/dist/server-7P67XIXT.js +15 -0
  44. package/dist/server-AB6BBV2H.js +15 -0
  45. package/dist/server-D2ZZ44LL.js +15 -0
  46. package/dist/server-UC23ZQIO.js +15 -0
  47. package/dist/setup-6UB3FST3.js +20 -0
  48. package/dist/setup-BW5BWSG2.js +20 -0
  49. package/dist/setup-KXN6MGWO.js +20 -0
  50. package/dist/setup-SZK7GYJE.js +20 -0
  51. package/dist/setup-X6CIHPCV.js +20 -0
  52. package/package.json +1 -1
  53. package/src/cli-agent.ts +43 -0
  54. package/src/dashboard/pages/agent-detail/configuration.js +383 -3
  55. package/src/engine/agent-routes.ts +32 -2
  56. package/src/engine/messaging-poller.ts +17 -1
  57. package/src/engine/routes.ts +2 -1
  58. package/src/runtime/gateway.ts +0 -45
@@ -205,6 +205,380 @@ function RoutingRowEditor(props) {
205
205
  );
206
206
  }
207
207
 
208
+ // ─── Model Cost Estimates (per 1K tokens, approximate USD) ───
209
+
210
+ var MODEL_COSTS = {
211
+ 'claude-opus-4-20250514': { input: 0.015, output: 0.075 },
212
+ 'claude-sonnet-4-20250514': { input: 0.003, output: 0.015 },
213
+ 'claude-haiku-3-20250414': { input: 0.00025, output: 0.00125 },
214
+ 'gpt-4o': { input: 0.005, output: 0.015 },
215
+ 'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
216
+ 'gemini-2.0-flash': { input: 0.0001, output: 0.0004 },
217
+ 'gemini-2.5-pro': { input: 0.00125, output: 0.01 },
218
+ };
219
+
220
+ function estimateHeartbeatCost(modelId, intervalMin, tokensPerBeat) {
221
+ tokensPerBeat = tokensPerBeat || 3000; // ~3K tokens per heartbeat round-trip
222
+ var key = Object.keys(MODEL_COSTS).find(function(k) { return modelId && modelId.indexOf(k) !== -1; });
223
+ var cost = key ? MODEL_COSTS[key] : { input: 0.003, output: 0.015 }; // default to sonnet pricing
224
+ var beatsPerDay = (24 * 60) / intervalMin;
225
+ var dailyCost = beatsPerDay * tokensPerBeat * ((cost.input + cost.output) / 2) / 1000;
226
+ return { beatsPerDay: Math.round(beatsPerDay), dailyCost: dailyCost, monthlyCost: dailyCost * 30 };
227
+ }
228
+
229
+ // ─── Model Fallback Card ───
230
+
231
+ function ModelFallbackCard(props) {
232
+ var config = props.config;
233
+ var saving = props.saving;
234
+ var providers = props.providers;
235
+ var saveUpdates = props.saveUpdates;
236
+
237
+ var fb = config.modelFallback || {};
238
+ var _editing = useState(false);
239
+ var editing = _editing[0]; var setEditing = _editing[1];
240
+ var _form = useState({});
241
+ var form = _form[0]; var setForm = _form[1];
242
+
243
+ function startEdit() {
244
+ setForm({
245
+ enabled: fb.enabled !== false,
246
+ fallbacks: (fb.fallbacks || []).slice(),
247
+ maxRetries: fb.maxRetries || 2,
248
+ retryDelayMs: fb.retryDelayMs || 1000,
249
+ });
250
+ setEditing(true);
251
+ }
252
+
253
+ function addFallback() {
254
+ setForm(function(f) { return Object.assign({}, f, { fallbacks: f.fallbacks.concat(['']) }); });
255
+ }
256
+
257
+ function removeFallback(idx) {
258
+ setForm(function(f) {
259
+ var next = f.fallbacks.slice();
260
+ next.splice(idx, 1);
261
+ return Object.assign({}, f, { fallbacks: next });
262
+ });
263
+ }
264
+
265
+ function updateFallback(idx, val) {
266
+ setForm(function(f) {
267
+ var next = f.fallbacks.slice();
268
+ next[idx] = val;
269
+ return Object.assign({}, f, { fallbacks: next });
270
+ });
271
+ }
272
+
273
+ function save() {
274
+ saveUpdates({
275
+ modelFallback: {
276
+ enabled: form.enabled,
277
+ fallbacks: form.fallbacks.filter(Boolean),
278
+ maxRetries: parseInt(form.maxRetries) || 2,
279
+ retryDelayMs: parseInt(form.retryDelayMs) || 1000,
280
+ }
281
+ }, function() { setEditing(false); });
282
+ }
283
+
284
+ var configuredProviders = providers.filter(function(p) { return p.configured; });
285
+
286
+ // Build flat list of all models from all configured providers for the dropdown
287
+ var _allModels = useState([]);
288
+ var allModels = _allModels[0]; var setAllModels = _allModels[1];
289
+ useEffect(function() {
290
+ if (!configuredProviders.length) return;
291
+ Promise.all(configuredProviders.map(function(p) {
292
+ return apiCall('/providers/' + p.id + '/models').then(function(d) {
293
+ return (d.models || []).map(function(m) { return { id: p.id + '/' + (m.id || m.name), label: p.name + ' / ' + (m.name || m.id) }; });
294
+ }).catch(function() { return []; });
295
+ })).then(function(results) {
296
+ setAllModels([].concat.apply([], results));
297
+ });
298
+ }, [providers]);
299
+
300
+ return h('div', { className: 'card', style: { padding: 20, marginBottom: 20 } },
301
+ h(CardHeader, {
302
+ title: 'Backup Model Providers',
303
+ help: h(HelpButton, { label: 'Backup Model Providers' },
304
+ h('p', null, 'Configure fallback models that activate automatically when the primary model fails (rate limits, outages, auth errors).'),
305
+ h('h4', { style: _h4 }, 'How It Works'),
306
+ h('ul', { style: _ul },
307
+ h('li', null, 'When the primary model returns an error, the system tries the next model in the chain.'),
308
+ h('li', null, h('strong', null, 'Rate limit / overload errors'), ' — retries the same model first (up to Max Retries), then moves to the next fallback.'),
309
+ h('li', null, h('strong', null, 'Auth / invalid model errors'), ' — skips immediately to the next fallback (no retry).'),
310
+ h('li', null, 'The chain is tried in order from top to bottom.')
311
+ ),
312
+ h('h4', { style: _h4 }, 'Settings'),
313
+ h('ul', { style: _ul },
314
+ h('li', null, h('strong', null, 'Max Retries'), ' — How many times to retry a single model before moving to the next one.'),
315
+ h('li', null, h('strong', null, 'Retry Delay'), ' — Wait time (ms) between retries. Increases with each attempt (exponential backoff).')
316
+ ),
317
+ h('div', { style: _tip }, h('strong', null, 'Tip: '), 'A good chain mixes providers — e.g., Anthropic primary → OpenAI fallback → Google fallback. This protects against single-provider outages.')
318
+ ),
319
+ editing: editing,
320
+ saving: saving,
321
+ onEdit: startEdit,
322
+ onSave: save,
323
+ onCancel: function() { setEditing(false); }
324
+ }),
325
+
326
+ editing
327
+ ? h(Fragment, null,
328
+ // Enable toggle
329
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 } },
330
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13 } },
331
+ h('input', { type: 'checkbox', checked: form.enabled, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { enabled: e.target.checked }); }); } }),
332
+ 'Enable model fallback'
333
+ )
334
+ ),
335
+
336
+ form.enabled && h(Fragment, null,
337
+ // Fallback chain
338
+ h('label', { style: labelStyle }, 'Fallback Chain (in priority order)'),
339
+ h('div', { style: { display: 'grid', gap: 8, marginBottom: 16 } },
340
+ form.fallbacks.map(function(fb, idx) {
341
+ return h('div', { key: idx, style: { display: 'flex', gap: 8, alignItems: 'center' } },
342
+ h('span', { style: { fontSize: 11, color: 'var(--text-muted)', width: 20, textAlign: 'center', flexShrink: 0 } }, '#' + (idx + 1)),
343
+ allModels.length > 0
344
+ ? h('select', { style: Object.assign({}, inputStyle, { flex: 1, cursor: 'pointer' }), value: fb, onChange: function(e) { updateFallback(idx, e.target.value); } },
345
+ h('option', { value: '' }, '-- Select model --'),
346
+ allModels.map(function(m) { return h('option', { key: m.id, value: m.id }, m.label); })
347
+ )
348
+ : h('input', { style: Object.assign({}, inputStyle, { flex: 1 }), value: fb, placeholder: 'provider/model-id (e.g. openai/gpt-4o)', onChange: function(e) { updateFallback(idx, e.target.value); } }),
349
+ h('button', { className: 'btn btn-ghost btn-sm', style: { color: 'var(--danger)', flexShrink: 0 }, onClick: function() { removeFallback(idx); } }, '\u2715')
350
+ );
351
+ }),
352
+ h('button', { className: 'btn btn-ghost btn-sm', onClick: addFallback, style: { justifySelf: 'start' } }, '+ Add Fallback Model')
353
+ ),
354
+
355
+ // Settings row
356
+ h('div', { style: rowStyle },
357
+ h('div', { style: fieldGroupStyle },
358
+ h('label', { style: labelStyle }, 'Max Retries per Model'),
359
+ h('select', { style: Object.assign({}, inputStyle, { cursor: 'pointer' }), value: form.maxRetries, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { maxRetries: e.target.value }); }); } },
360
+ h('option', { value: 1 }, '1'),
361
+ h('option', { value: 2 }, '2 (recommended)'),
362
+ h('option', { value: 3 }, '3'),
363
+ h('option', { value: 5 }, '5')
364
+ )
365
+ ),
366
+ h('div', { style: fieldGroupStyle },
367
+ h('label', { style: labelStyle }, 'Retry Delay (ms)'),
368
+ h('select', { style: Object.assign({}, inputStyle, { cursor: 'pointer' }), value: form.retryDelayMs, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { retryDelayMs: e.target.value }); }); } },
369
+ h('option', { value: 500 }, '500ms'),
370
+ h('option', { value: 1000 }, '1,000ms (recommended)'),
371
+ h('option', { value: 2000 }, '2,000ms'),
372
+ h('option', { value: 5000 }, '5,000ms')
373
+ )
374
+ )
375
+ )
376
+ )
377
+ )
378
+ : h(Fragment, null,
379
+ fb.enabled === false
380
+ ? h('span', { style: { fontSize: 13, color: 'var(--text-muted)' } }, 'Model fallback is disabled.')
381
+ : (fb.fallbacks || []).length > 0
382
+ ? h(Fragment, null,
383
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 } },
384
+ h('span', { className: 'badge badge-success', style: { fontSize: 11 } }, 'Enabled'),
385
+ h('span', { style: { fontSize: 12, color: 'var(--text-muted)' } }, (fb.fallbacks.length) + ' fallback' + (fb.fallbacks.length > 1 ? 's' : '') + ' \u00B7 ' + (fb.maxRetries || 2) + ' retries \u00B7 ' + (fb.retryDelayMs || 1000) + 'ms delay')
386
+ ),
387
+ h('div', { style: { display: 'grid', gap: 6 } },
388
+ fb.fallbacks.map(function(m, i) {
389
+ return h('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', background: 'var(--bg-secondary)', borderRadius: 'var(--radius)' } },
390
+ h('span', { style: { fontSize: 11, color: 'var(--text-muted)', width: 20 } }, '#' + (i + 1)),
391
+ h('span', { className: 'badge badge-info', style: { fontSize: 11, fontFamily: 'var(--font-mono)' } }, m.split('/').pop()),
392
+ h('span', { style: { fontSize: 11, color: 'var(--text-muted)' } }, m.split('/')[0])
393
+ );
394
+ })
395
+ )
396
+ )
397
+ : h('span', { style: { fontSize: 13, color: 'var(--text-muted)' } }, 'No fallback models configured \u2014 if the primary model fails, requests will error.')
398
+ )
399
+ );
400
+ }
401
+
402
+ // ─── Heartbeat Configuration Card ───
403
+
404
+ function HeartbeatCard(props) {
405
+ var config = props.config;
406
+ var saving = props.saving;
407
+ var modelObj = props.modelObj;
408
+ var modelStr = props.modelStr;
409
+ var saveUpdates = props.saveUpdates;
410
+
411
+ var hb = config.heartbeat || {};
412
+ var _editing = useState(false);
413
+ var editing = _editing[0]; var setEditing = _editing[1];
414
+ var _form = useState({});
415
+ var form = _form[0]; var setForm = _form[1];
416
+
417
+ function startEdit() {
418
+ setForm({
419
+ enabled: hb.enabled !== false,
420
+ intervalMinutes: hb.intervalMinutes || 30,
421
+ activeHoursStart: hb.activeHoursStart != null ? hb.activeHoursStart : 8,
422
+ activeHoursEnd: hb.activeHoursEnd != null ? hb.activeHoursEnd : 23,
423
+ prompt: hb.prompt || '',
424
+ tokensPerBeat: hb.tokensPerBeat || 3000,
425
+ });
426
+ setEditing(true);
427
+ }
428
+
429
+ function save() {
430
+ saveUpdates({
431
+ heartbeat: {
432
+ enabled: form.enabled,
433
+ intervalMinutes: parseInt(form.intervalMinutes) || 30,
434
+ activeHoursStart: parseInt(form.activeHoursStart),
435
+ activeHoursEnd: parseInt(form.activeHoursEnd),
436
+ prompt: form.prompt || '',
437
+ tokensPerBeat: parseInt(form.tokensPerBeat) || 3000,
438
+ }
439
+ }, function() { setEditing(false); });
440
+ }
441
+
442
+ var modelId = modelStr || modelObj.modelId || '';
443
+ var est = estimateHeartbeatCost(modelId, editing ? (parseInt(form.intervalMinutes) || 30) : (hb.intervalMinutes || 30), editing ? (parseInt(form.tokensPerBeat) || 3000) : (hb.tokensPerBeat || 3000));
444
+
445
+ var INTERVAL_OPTIONS = [
446
+ { value: 5, label: 'Every 5 minutes' },
447
+ { value: 10, label: 'Every 10 minutes' },
448
+ { value: 15, label: 'Every 15 minutes' },
449
+ { value: 30, label: 'Every 30 minutes (recommended)' },
450
+ { value: 60, label: 'Every 1 hour' },
451
+ { value: 120, label: 'Every 2 hours' },
452
+ { value: 360, label: 'Every 6 hours' },
453
+ { value: 720, label: 'Every 12 hours' },
454
+ { value: 1440, label: 'Once a day' },
455
+ ];
456
+
457
+ var hours = [];
458
+ for (var i = 0; i < 24; i++) { hours.push({ value: i, label: (i === 0 ? '12' : i > 12 ? '' + (i - 12) : '' + i) + ':00 ' + (i < 12 ? 'AM' : 'PM') }); }
459
+
460
+ return h('div', { className: 'card', style: { padding: 20, marginBottom: 20 } },
461
+ h(CardHeader, {
462
+ title: 'Heartbeat',
463
+ help: h(HelpButton, { label: 'Heartbeat Configuration' },
464
+ h('p', null, 'Heartbeats are periodic check-ins where the agent wakes up, checks for pending work (emails, calendar, notifications), and takes proactive action.'),
465
+ h('h4', { style: _h4 }, 'Settings'),
466
+ h('ul', { style: _ul },
467
+ h('li', null, h('strong', null, 'Interval'), ' — How often the agent checks in. Shorter = more responsive but more expensive.'),
468
+ h('li', null, h('strong', null, 'Active Hours'), ' — Only run heartbeats during these hours (agent\'s timezone). Saves cost overnight.'),
469
+ h('li', null, h('strong', null, 'Custom Prompt'), ' — Override the default heartbeat prompt. Use this to tell the agent what to check during heartbeats.'),
470
+ h('li', null, h('strong', null, 'Tokens per Beat'), ' — Estimated tokens consumed per heartbeat cycle (used for cost projection).')
471
+ ),
472
+ h('h4', { style: _h4 }, 'Cost Impact'),
473
+ h('p', null, 'Each heartbeat is a full LLM call. A 30-minute interval = 48 calls/day. The cost estimate below uses your current default model\'s pricing.'),
474
+ h('div', { style: _tip }, h('strong', null, 'Tip: '), 'Use a cheaper model for heartbeats via Model Routing (set the "Scheduling" context). A 30-min interval with Haiku costs ~$0.10/day vs ~$5/day with Opus.')
475
+ ),
476
+ editing: editing,
477
+ saving: saving,
478
+ onEdit: startEdit,
479
+ onSave: save,
480
+ onCancel: function() { setEditing(false); }
481
+ }),
482
+
483
+ // Cost estimate banner (always visible)
484
+ h('div', { style: { display: 'flex', gap: 16, padding: '12px 16px', background: 'var(--bg-secondary)', borderRadius: 'var(--radius)', marginBottom: 16, alignItems: 'center' } },
485
+ h('div', { style: { flex: 1 } },
486
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginBottom: 2 } }, 'Estimated Cost'),
487
+ h('div', { style: { fontSize: 16, fontWeight: 700 } }, '$' + est.dailyCost.toFixed(2) + '/day'),
488
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, '\u2248 $' + est.monthlyCost.toFixed(2) + '/month')
489
+ ),
490
+ h('div', { style: { flex: 1 } },
491
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginBottom: 2 } }, 'Beats/Day'),
492
+ h('div', { style: { fontSize: 16, fontWeight: 700 } }, '' + est.beatsPerDay)
493
+ ),
494
+ h('div', { style: { flex: 1 } },
495
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginBottom: 2 } }, 'Model'),
496
+ h('div', { style: { fontSize: 12, fontFamily: 'var(--font-mono)' } }, modelId ? modelId.split('/').pop() : 'Not set')
497
+ )
498
+ ),
499
+
500
+ editing
501
+ ? h(Fragment, null,
502
+ // Enable toggle
503
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 } },
504
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13 } },
505
+ h('input', { type: 'checkbox', checked: form.enabled, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { enabled: e.target.checked }); }); } }),
506
+ 'Enable heartbeat'
507
+ )
508
+ ),
509
+
510
+ form.enabled && h(Fragment, null,
511
+ // Interval
512
+ h('div', { style: fieldGroupStyle },
513
+ h('label', { style: labelStyle }, 'Interval'),
514
+ h('select', { style: Object.assign({}, inputStyle, { cursor: 'pointer' }), value: form.intervalMinutes, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { intervalMinutes: e.target.value }); }); } },
515
+ INTERVAL_OPTIONS.map(function(o) { return h('option', { key: o.value, value: o.value }, o.label); })
516
+ )
517
+ ),
518
+
519
+ // Active hours
520
+ h('div', { style: rowStyle },
521
+ h('div', { style: fieldGroupStyle },
522
+ h('label', { style: labelStyle }, 'Active From'),
523
+ h('select', { style: Object.assign({}, inputStyle, { cursor: 'pointer' }), value: form.activeHoursStart, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { activeHoursStart: e.target.value }); }); } },
524
+ hours.map(function(hr) { return h('option', { key: hr.value, value: hr.value }, hr.label); })
525
+ )
526
+ ),
527
+ h('div', { style: fieldGroupStyle },
528
+ h('label', { style: labelStyle }, 'Active Until'),
529
+ h('select', { style: Object.assign({}, inputStyle, { cursor: 'pointer' }), value: form.activeHoursEnd, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { activeHoursEnd: e.target.value }); }); } },
530
+ hours.map(function(hr) { return h('option', { key: hr.value, value: hr.value }, hr.label); })
531
+ )
532
+ )
533
+ ),
534
+ h('p', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: -8, marginBottom: 16 } },
535
+ 'Heartbeats only run between these hours. Set to 12:00 AM \u2013 12:00 AM for 24/7.'
536
+ ),
537
+
538
+ // Tokens per beat
539
+ h('div', { style: fieldGroupStyle },
540
+ h('label', { style: labelStyle }, 'Estimated Tokens per Beat'),
541
+ h('select', { style: Object.assign({}, inputStyle, { cursor: 'pointer' }), value: form.tokensPerBeat, onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { tokensPerBeat: e.target.value }); }); } },
542
+ h('option', { value: 1000 }, '~1K (minimal — just HEARTBEAT_OK)'),
543
+ h('option', { value: 3000 }, '~3K (typical — light checks)'),
544
+ h('option', { value: 5000 }, '~5K (moderate — email/calendar checks)'),
545
+ h('option', { value: 10000 }, '~10K (heavy — full inbox scan + actions)')
546
+ )
547
+ ),
548
+
549
+ // Custom prompt
550
+ h('div', { style: fieldGroupStyle },
551
+ h('label', { style: labelStyle }, 'Custom Heartbeat Prompt (optional)'),
552
+ h('textarea', {
553
+ style: Object.assign({}, inputStyle, { minHeight: 60, resize: 'vertical', fontFamily: 'var(--font-mono)', fontSize: 12 }),
554
+ value: form.prompt,
555
+ onChange: function(e) { setForm(function(f) { return Object.assign({}, f, { prompt: e.target.value }); }); },
556
+ placeholder: 'Leave empty for default. E.g.: Check emails, calendar events in next 2h, and Slack mentions.'
557
+ }),
558
+ h('p', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } }, 'This prompt is sent to the agent each heartbeat cycle. The agent can reply HEARTBEAT_OK if nothing needs attention.')
559
+ )
560
+ )
561
+ )
562
+ : h(Fragment, null,
563
+ hb.enabled === false
564
+ ? h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
565
+ h('span', { className: 'badge badge-neutral', style: { fontSize: 11 } }, 'Disabled'),
566
+ h('span', { style: { fontSize: 13, color: 'var(--text-muted)' } }, 'Heartbeat is turned off. Agent won\'t check in proactively.')
567
+ )
568
+ : h(Fragment, null,
569
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 } },
570
+ h('span', { className: 'badge badge-success', style: { fontSize: 11 } }, 'Enabled'),
571
+ h('span', { style: { fontSize: 12, color: 'var(--text-muted)' } },
572
+ 'Every ' + (hb.intervalMinutes || 30) + ' min' +
573
+ (hb.activeHoursStart != null ? ' \u00B7 ' + (hb.activeHoursStart === 0 ? '12' : hb.activeHoursStart > 12 ? (hb.activeHoursStart - 12) : hb.activeHoursStart) + ':00' + (hb.activeHoursStart < 12 ? 'AM' : 'PM') + '\u2013' + (hb.activeHoursEnd === 0 ? '12' : hb.activeHoursEnd > 12 ? (hb.activeHoursEnd - 12) : hb.activeHoursEnd) + ':00' + (hb.activeHoursEnd < 12 ? 'AM' : 'PM') : ' \u00B7 24/7')
574
+ )
575
+ ),
576
+ hb.prompt && h('div', { style: { padding: '8px 12px', background: 'var(--bg-secondary)', borderRadius: 'var(--radius)', fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', whiteSpace: 'pre-wrap', maxHeight: 80, overflow: 'auto' } }, hb.prompt)
577
+ )
578
+ )
579
+ );
580
+ }
581
+
208
582
  // ─── Main Component ───
209
583
 
210
584
  export function ConfigurationSection(props) {
@@ -422,7 +796,13 @@ export function ConfigurationSection(props) {
422
796
  )
423
797
  ),
424
798
 
425
- // ═══ Card 3: Meeting Voice ═══
799
+ // ═══ Card 3: Model Fallback / Backup Providers ═══
800
+ h(ModelFallbackCard, { config: config, saving: saving, providers: providers, saveUpdates: saveUpdates }),
801
+
802
+ // ═══ Card 4: Heartbeat Configuration ═══
803
+ h(HeartbeatCard, { config: config, saving: saving, modelObj: modelObj, modelStr: modelStr, saveUpdates: saveUpdates }),
804
+
805
+ // ═══ Card 5: Meeting Voice ═══
426
806
  h('div', { className: 'card', style: { padding: 20, marginBottom: 20 } },
427
807
  h(CardHeader, {
428
808
  title: 'Meeting Voice (ElevenLabs)',
@@ -465,7 +845,7 @@ export function ConfigurationSection(props) {
465
845
  : h('span', { style: { fontSize: 13, color: 'var(--text-muted)' } }, 'No voice configured \u2014 agent uses text only in meetings')
466
846
  ),
467
847
 
468
- // ═══ Card 4: Description ═══
848
+ // ═══ Card 6: Description ═══
469
849
  h('div', { className: 'card', style: { padding: 20, marginBottom: 20 } },
470
850
  h(CardHeader, {
471
851
  title: 'Description',
@@ -493,7 +873,7 @@ export function ConfigurationSection(props) {
493
873
  : h('div', { style: { fontSize: 14, color: displayDescription ? 'var(--text-primary)' : 'var(--text-muted)', lineHeight: 1.6 } }, displayDescription || 'No description set.')
494
874
  ),
495
875
 
496
- // ═══ Card 5: Soul Template (only if set) ═══
876
+ // ═══ Card 7: Soul Template (only if set) ═══
497
877
  config.soulId && h('div', { className: 'card', style: { padding: 20 } },
498
878
  h('h4', { style: { margin: '0 0 16px', fontSize: 14, fontWeight: 600 } }, 'Role Template'),
499
879
  h('span', { className: 'badge badge-primary' }, config.soulId.replace(/-/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); }))
@@ -56,16 +56,46 @@ export function createAgentRoutes(opts: {
56
56
  router.patch('/agents/:id/config', async (c) => {
57
57
  const { updates, updatedBy } = await c.req.json();
58
58
  try {
59
+ const agentId = c.req.param('id');
59
60
  const actor = c.req.header('X-User-Id') || updatedBy;
60
- const agent = await lifecycle.updateConfig(c.req.param('id'), updates, actor);
61
+
62
+ // Capture old deployment config for change detection
63
+ const oldAgent = lifecycle.getAgent(agentId);
64
+ const oldDep = oldAgent?.config?.deployment;
65
+
66
+ const agent = await lifecycle.updateConfig(agentId, updates, actor);
67
+
61
68
  // Sync name/email to admin agents table
62
69
  const adminDb = getAdminDb();
63
70
  if (adminDb && (updates.name || updates.email)) {
64
71
  const sync: any = {};
65
72
  if (updates.name) sync.name = updates.name;
66
73
  if (updates.email) sync.email = updates.email;
67
- adminDb.updateAgent(c.req.param('id'), sync).catch(() => {});
74
+ adminDb.updateAgent(agentId, sync).catch(() => {});
68
75
  }
76
+
77
+ // Auto-restart agent if deployment config changed (port, host, target)
78
+ if (updates.deployment) {
79
+ const newDep = agent.config?.deployment;
80
+ const portChanged = oldDep?.port !== newDep?.port;
81
+ const hostChanged = oldDep?.host !== newDep?.host;
82
+ const targetChanged = oldDep?.target !== newDep?.target;
83
+ if (portChanged || hostChanged || targetChanged) {
84
+ console.log(`[agent-routes] Deployment config changed for ${agent.name || agentId} (port: ${oldDep?.port}→${newDep?.port}, host: ${oldDep?.host}→${newDep?.host}). Triggering agent restart...`);
85
+ // Try PM2 restart for locally deployed agents
86
+ try {
87
+ const { exec } = await import('node:child_process');
88
+ const pm2Name = (agent.name || '').toLowerCase().replace(/\s+/g, '-') + '-agent';
89
+ exec(`pm2 restart ${pm2Name} --update-env 2>/dev/null || pm2 restart ${agentId} --update-env 2>/dev/null`, (err) => {
90
+ if (err) console.warn(`[agent-routes] PM2 restart for ${pm2Name} failed (may not be PM2-managed): ${err.message}`);
91
+ else console.log(`[agent-routes] PM2 restart triggered for ${pm2Name}`);
92
+ });
93
+ } catch (e: any) {
94
+ console.warn(`[agent-routes] Agent restart failed: ${e.message}`);
95
+ }
96
+ }
97
+ }
98
+
69
99
  return c.json({ agent });
70
100
  } catch (e: any) {
71
101
  return c.json({ error: e.message }, 400);
@@ -49,6 +49,20 @@ export class MessagingPoller {
49
49
  this.config = config;
50
50
  }
51
51
 
52
+ /** Resolve fresh agent endpoint from lifecycle (picks up port/host changes) */
53
+ private resolveEndpoint(agentId: string, fallback: AgentEndpoint): AgentEndpoint {
54
+ try {
55
+ var managed = this.config.lifecycle.getAgent(agentId);
56
+ if (managed) {
57
+ var dep = managed.config?.deployment;
58
+ var port = dep?.port || dep?.config?.local?.port || fallback.port;
59
+ var host = dep?.host || dep?.config?.local?.host || fallback.host;
60
+ return { ...fallback, port, host };
61
+ }
62
+ } catch {}
63
+ return fallback;
64
+ }
65
+
52
66
  async start() {
53
67
  if (this.running) return;
54
68
  this.running = true;
@@ -437,7 +451,9 @@ export class MessagingPoller {
437
451
  }).catch(() => {});
438
452
  }
439
453
  try {
440
- var resp = await fetch(`http://${agent.host}:${agent.port}/api/runtime/chat`, {
454
+ var resolved = this.resolveEndpoint(agent.id, agent);
455
+ console.log(`[messaging] Dispatching to ${agent.displayName} at ${resolved.host}:${resolved.port}`);
456
+ var resp = await fetch(`http://${resolved.host}:${resolved.port}/api/runtime/chat`, {
441
457
  method: 'POST',
442
458
  headers: { 'Content-Type': 'application/json' },
443
459
  body: JSON.stringify({
@@ -684,10 +684,11 @@ let _messagingPoller: MessagingPoller | null = null;
684
684
 
685
685
  async function startMessagingPoller(engineDb: any): Promise<void> {
686
686
  const allAgents = lifecycle.getAllAgents();
687
- const agents = allAgents.filter(a => a.state === 'running' || (a as any).status === 'active').map(a => {
687
+ const agents = allAgents.filter(a => a.state === 'running' || a.state === 'draft' || a.state === 'stopped' || (a as any).status === 'active').map(a => {
688
688
  const dep = a.config?.deployment;
689
689
  const port = dep?.port || dep?.config?.local?.port || 3100;
690
690
  const host = dep?.host || dep?.config?.local?.host || 'localhost';
691
+ // Debug removed — dynamic resolution in messaging-poller.resolveEndpoint()
691
692
  return {
692
693
  id: a.id, name: a.name || '', displayName: (a.config as any)?.displayName || a.name || a.id,
693
694
  status: 'active' as const, port, host,
@@ -245,51 +245,6 @@ export function createRuntimeGateway(config: GatewayConfig): Hono {
245
245
 
246
246
  // ─── Inbound Email Hook ──────────────────────────
247
247
 
248
- // ─── Chat dispatch (from enterprise messaging/chat pollers) ──
249
- app.post('/chat', async function(c) {
250
- try {
251
- var body = await c.req.json();
252
- // Find or create a session for this chat context
253
- var source = body.source || body.spaceName || 'unknown';
254
- var spaceId = body.spaceId || body.chatId || 'default';
255
- var sessionTag = source + ':' + spaceId;
256
-
257
- // Look for existing session with this tag
258
- var sessions = await runtime.listSessions();
259
- var existing = sessions.find(function(s: any) { return s.tag === sessionTag && s.status === 'active'; });
260
- var session: any;
261
-
262
- if (existing) {
263
- session = existing;
264
- } else {
265
- // Create new session
266
- session = await runtime.createSession({
267
- tag: sessionTag,
268
- metadata: { source: source, spaceId: spaceId, senderName: body.senderName, isDM: body.isDM },
269
- });
270
- }
271
-
272
- // Send message to the session
273
- await runtime.sendMessage(session.id, body.messageText || body.message || '', {
274
- senderName: body.senderName,
275
- senderEmail: body.senderEmail,
276
- source: source,
277
- isDM: body.isDM,
278
- isManager: body.isManager,
279
- priority: body.priority,
280
- messageId: body.messageId,
281
- isCustomer: body.isCustomer,
282
- customerSystemPrompt: body.customerSystemPrompt,
283
- restrictTools: body.restrictTools,
284
- });
285
-
286
- return c.json({ ok: true, sessionId: session.id });
287
- } catch (err: any) {
288
- console.error('[runtime] /chat error:', err.message);
289
- return c.json({ error: err.message }, 500);
290
- }
291
- });
292
-
293
248
  app.post('/hooks/inbound', async function(c) {
294
249
  try {
295
250
  var body = await c.req.json();