neoagent 3.0.1-beta.9 → 3.1.0

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 (97) hide show
  1. package/.env.example +2 -10
  2. package/README.md +12 -3
  3. package/docs/automation.md +37 -0
  4. package/docs/benchmarking.md +102 -0
  5. package/docs/billing.md +34 -8
  6. package/docs/configuration.md +11 -2
  7. package/docs/getting-started.md +10 -8
  8. package/docs/operations.md +1 -1
  9. package/flutter_app/lib/main.dart +3 -0
  10. package/flutter_app/lib/main_account_settings.dart +138 -0
  11. package/flutter_app/lib/main_app_shell.dart +38 -13
  12. package/flutter_app/lib/main_billing.dart +1465 -0
  13. package/flutter_app/lib/main_chat.dart +1612 -214
  14. package/flutter_app/lib/main_controller.dart +398 -26
  15. package/flutter_app/lib/main_devices.dart +293 -207
  16. package/flutter_app/lib/main_models.dart +142 -0
  17. package/flutter_app/lib/main_navigation.dart +19 -1
  18. package/flutter_app/lib/main_operations.dart +288 -9
  19. package/flutter_app/lib/main_settings.dart +510 -300
  20. package/flutter_app/lib/main_shared.dart +2 -0
  21. package/flutter_app/lib/main_timeline.dart +1378 -0
  22. package/flutter_app/lib/src/backend_client.dart +149 -19
  23. package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
  24. package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
  25. package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
  26. package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
  27. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
  28. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
  29. package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
  30. package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
  31. package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
  32. package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
  33. package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
  34. package/flutter_app/lib/src/recording_payloads.dart +9 -0
  35. package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
  36. package/flutter_app/windows/runner/flutter_window.cpp +75 -0
  37. package/landing/index.html +3 -3
  38. package/lib/manager.js +184 -66
  39. package/package.json +4 -1
  40. package/server/admin/access.js +12 -7
  41. package/server/admin/admin.js +436 -10
  42. package/server/admin/billing.js +158 -35
  43. package/server/admin/index.html +72 -2
  44. package/server/admin/users.js +15 -15
  45. package/server/db/database.js +125 -20
  46. package/server/http/routes.js +1 -0
  47. package/server/http/static.js +4 -2
  48. package/server/public/.last_build_id +1 -1
  49. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  50. package/server/public/canvaskit/wimp.js.symbols +8475 -8467
  51. package/server/public/canvaskit/wimp.wasm +0 -0
  52. package/server/public/flutter_bootstrap.js +2 -2
  53. package/server/public/main.dart.js +91077 -87037
  54. package/server/routes/account.js +53 -0
  55. package/server/routes/admin.js +345 -64
  56. package/server/routes/agents.js +203 -21
  57. package/server/routes/billing.js +5 -0
  58. package/server/routes/browser.js +8 -1
  59. package/server/routes/recordings.js +96 -6
  60. package/server/routes/screenHistory.js +140 -2
  61. package/server/routes/timeline.js +43 -0
  62. package/server/services/account/erasure.js +263 -0
  63. package/server/services/ai/hooks.js +4 -1
  64. package/server/services/ai/loop/agent_engine_core.js +8 -1
  65. package/server/services/ai/loop/blank_recovery.js +36 -0
  66. package/server/services/ai/loop/conversation_loop.js +166 -33
  67. package/server/services/ai/messagingFallback.js +22 -0
  68. package/server/services/ai/rate_limits.js +28 -5
  69. package/server/services/ai/systemPrompt.js +6 -5
  70. package/server/services/ai/taskAnalysis.js +2 -0
  71. package/server/services/ai/toolEvidence.js +15 -0
  72. package/server/services/ai/toolResult.js +40 -0
  73. package/server/services/ai/tools.js +163 -4
  74. package/server/services/android/controller.js +6 -2
  75. package/server/services/billing/plans.js +2 -1
  76. package/server/services/browser/anti_detection.js +192 -0
  77. package/server/services/browser/controller.js +180 -54
  78. package/server/services/desktop/auth.js +3 -0
  79. package/server/services/desktop/registry.js +50 -2
  80. package/server/services/integrations/google/calendar.js +22 -14
  81. package/server/services/manager.js +12 -42
  82. package/server/services/memory/ingestion_chunking.js +268 -0
  83. package/server/services/messaging/telnyx.js +9 -8
  84. package/server/services/recordings/manager.js +60 -27
  85. package/server/services/runtime/docker-vm-manager.js +157 -266
  86. package/server/services/runtime/guest_bootstrap.js +17 -5
  87. package/server/services/runtime/guest_image.js +188 -0
  88. package/server/services/runtime/manager.js +0 -1
  89. package/server/services/runtime/validation.js +3 -8
  90. package/server/services/social_video/service.js +60 -10
  91. package/server/services/tasks/runtime.js +234 -9
  92. package/server/services/tasks/task_repository.js +13 -0
  93. package/server/services/timeline/service.js +558 -0
  94. package/server/services/wearable/gateway.js +1 -1
  95. package/server/services/websocket.js +21 -3
  96. package/server/services/desktop/screenRecorder.js +0 -292
  97. package/server/services/desktop/screen_recorder_support.js +0 -46
@@ -19,6 +19,10 @@ const { isTransientError } = require('../ai/providerRetry');
19
19
  const MAX_AUTONOMOUS_RETRIES = 1;
20
20
  const MAX_RECURRING_TASK_START_DELAY_MS = 90 * 1000;
21
21
  const INTEGRATION_TRIGGER_POLL_CRON = '* * * * *';
22
+ const DEFAULT_TASK_LOOP_BUDGET = Object.freeze({
23
+ maxRunsPerDay: 24,
24
+ maxTokensPerDay: 250000,
25
+ });
22
26
 
23
27
  function normalizeStoredString(value) {
24
28
  if (value == null) return '';
@@ -66,6 +70,37 @@ function stringifyTaskResult(result) {
66
70
  return '';
67
71
  }
68
72
 
73
+ function finitePositiveInteger(value, fallback, max = Number.MAX_SAFE_INTEGER) {
74
+ const parsed = Number(value);
75
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
76
+ return Math.min(Math.floor(parsed), max);
77
+ }
78
+
79
+ function normalizeLoopBudgetConfig(taskConfig = {}) {
80
+ const raw = taskConfig.loopBudget && typeof taskConfig.loopBudget === 'object' && !Array.isArray(taskConfig.loopBudget)
81
+ ? taskConfig.loopBudget
82
+ : {};
83
+ const disabled = raw.enabled === false || raw.enabled === 'false';
84
+ const paused = raw.paused === true
85
+ || raw.pause === true
86
+ || taskConfig.loopPaused === true
87
+ || taskConfig.loop_paused === true;
88
+ return {
89
+ enabled: !disabled,
90
+ paused,
91
+ maxRunsPerDay: finitePositiveInteger(
92
+ raw.maxRunsPerDay ?? raw.max_runs_per_day,
93
+ DEFAULT_TASK_LOOP_BUDGET.maxRunsPerDay,
94
+ 500,
95
+ ),
96
+ maxTokensPerDay: finitePositiveInteger(
97
+ raw.maxTokensPerDay ?? raw.max_tokens_per_day,
98
+ DEFAULT_TASK_LOOP_BUDGET.maxTokensPerDay,
99
+ 20_000_000,
100
+ ),
101
+ };
102
+ }
103
+
69
104
  class TaskRuntime {
70
105
  constructor(io, agentEngine, app = null, options = {}) {
71
106
  this.io = io;
@@ -90,6 +125,10 @@ class TaskRuntime {
90
125
  return this.app?.locals?.integrationManager || null;
91
126
  }
92
127
 
128
+ get timelineService() {
129
+ return this.app?.locals?.timelineService || null;
130
+ }
131
+
93
132
  getStatus() {
94
133
  return {
95
134
  state: this.state,
@@ -377,9 +416,18 @@ class TaskRuntime {
377
416
  if (!task.enabled) return;
378
417
  if ((task.trigger_type || 'schedule') !== 'schedule') return;
379
418
  const triggerConfig = this._normalizeJson(task.trigger_config);
380
- if (triggerConfig.mode === 'one_time') return;
381
- const cronExpression = String(triggerConfig.cronExpression || '').trim();
382
- if (!cronExpression) return;
419
+ // Resolve the cron expression from the structured trigger_config, falling
420
+ // back to the legacy cron_expression column. An older migration could leave
421
+ // the expression in only one of the two places; as long as either still
422
+ // holds it, the task must keep being scheduled.
423
+ const cronExpression = String(
424
+ triggerConfig.cronExpression || task.cron_expression || '',
425
+ ).trim();
426
+ // One-time runs are driven by the one-time poller, not node-cron. Only skip
427
+ // here when there is genuinely no recurring cron expression to honor.
428
+ if (!cronExpression) {
429
+ return;
430
+ }
383
431
  const job = this.cron.schedule(cronExpression, async () => {
384
432
  try {
385
433
  await this._executeTask(task.id, task.user_id, {
@@ -410,6 +458,16 @@ class TaskRuntime {
410
458
  }
411
459
  const executionKey = `${userId}:${taskId}`;
412
460
  if (this.runningTaskExecutions.has(executionKey)) {
461
+ this._recordTaskLifecycle({
462
+ userId,
463
+ taskId,
464
+ taskName: this.taskRepository.getTaskById(taskId, userId)?.name || `Task ${taskId}`,
465
+ agentId: this.taskRepository.getTaskById(taskId, userId)?.agent_id || null,
466
+ eventKind: 'task_skipped',
467
+ reason: 'already_running_or_queued',
468
+ triggerType: executionMeta.triggerType || null,
469
+ triggerSource: executionMeta.triggerSource || null,
470
+ });
413
471
  this.io.to(`user:${userId}`).emit('tasks:task_skipped', {
414
472
  taskId,
415
473
  reason: 'already_running_or_queued',
@@ -445,6 +503,16 @@ class TaskRuntime {
445
503
  && (Date.now() - scheduledAtMs) > MAX_RECURRING_TASK_START_DELAY_MS
446
504
  );
447
505
  if (isLateRecurringRun) {
506
+ this._recordTaskLifecycle({
507
+ userId,
508
+ taskId,
509
+ taskName: task.name || `Task ${taskId}`,
510
+ agentId,
511
+ eventKind: 'task_skipped',
512
+ reason: 'stale_start_delay',
513
+ triggerType: executionMeta.triggerType || null,
514
+ triggerSource: executionMeta.triggerSource || null,
515
+ });
448
516
  this.io.to(`user:${userId}`).emit('tasks:task_skipped', {
449
517
  taskId,
450
518
  reason: 'stale_start_delay',
@@ -454,14 +522,52 @@ class TaskRuntime {
454
522
  return { skipped: true, reason: 'stale_start_delay' };
455
523
  }
456
524
 
525
+ const budgetDecision = this._evaluateTaskLoopBudget(task, taskConfig, userId, {
526
+ manual: executionMeta.manual === true,
527
+ });
528
+ if (budgetDecision.mode === 'paused' || budgetDecision.mode === 'exhausted') {
529
+ this._recordTaskLifecycle({
530
+ userId,
531
+ taskId,
532
+ taskName: task.name || `Task ${taskId}`,
533
+ agentId,
534
+ eventKind: 'task_skipped',
535
+ reason: budgetDecision.reason,
536
+ triggerType: executionMeta.triggerType || null,
537
+ triggerSource: executionMeta.triggerSource || null,
538
+ });
539
+ this.io.to(`user:${userId}`).emit('tasks:task_skipped', {
540
+ taskId,
541
+ reason: budgetDecision.reason,
542
+ budget: budgetDecision.snapshot,
543
+ timestamp: new Date().toISOString(),
544
+ });
545
+ return {
546
+ skipped: true,
547
+ reason: budgetDecision.reason,
548
+ budget: budgetDecision.snapshot,
549
+ };
550
+ }
551
+
457
552
  this.taskRepository.markTaskRun(taskId, userId);
458
553
  this.io.to(`user:${userId}`).emit('tasks:task_running', { taskId, timestamp: new Date().toISOString() });
554
+ this._recordTaskLifecycle({
555
+ userId,
556
+ taskId,
557
+ taskName: task.name || `Task ${taskId}`,
558
+ agentId,
559
+ eventKind: 'task_started',
560
+ triggerType: executionMeta.triggerType || null,
561
+ triggerSource: executionMeta.triggerSource || null,
562
+ });
459
563
 
460
564
  let normalizedConfig = taskConfig;
461
565
  const taskName = task.name || `Task ${taskId}`;
462
566
  const deliveryState = {
463
567
  messagingSent: false,
464
568
  noResponse: false,
569
+ proactiveMessageStaged: false,
570
+ stagedProactiveMessage: null,
465
571
  lastSentMessage: '',
466
572
  sentMessages: [],
467
573
  };
@@ -478,6 +584,15 @@ class TaskRuntime {
478
584
  scheduledAt: executionMeta.scheduledAt || null,
479
585
  });
480
586
  this.io.to(`user:${userId}`).emit('tasks:task_complete', { taskId, result });
587
+ this._recordTaskLifecycle({
588
+ userId,
589
+ taskId,
590
+ taskName,
591
+ agentId,
592
+ eventKind: 'task_completed',
593
+ triggerType: executionMeta.triggerType || null,
594
+ triggerSource: executionMeta.triggerSource || null,
595
+ });
481
596
  return result;
482
597
  }
483
598
 
@@ -489,7 +604,7 @@ class TaskRuntime {
489
604
  if (normalizedConfig.callTo) {
490
605
  notifyHint = `\n\nThis task is configured to notify the user by phone. Use the make_call tool to call "${normalizedConfig.callTo}" with an appropriate greeting based on your findings. The configured greeting hint is: "${normalizedConfig.callGreeting || 'Hello, this is your task reminder.'}"`;
491
606
  } else if (normalizedConfig.notifyPlatform && normalizedConfig.notifyTo) {
492
- notifyHint = `\n\nIf your task result is worth notifying the user about, send it proactively via send_message to platform="${normalizedConfig.notifyPlatform}" to="${normalizedConfig.notifyTo}" and set purpose="final_result" for a concrete useful outcome or purpose="blocker" for a real issue the user should know about. If nothing important or actionable changed, call send_message with purpose="no_response" and content="[NO RESPONSE]".${manualRun ? '' : ' For this automatic scheduled run, plain assistant text is internal only and is NOT delivered. You MUST end the run with exactly one explicit send_message decision (purpose="final_result", "blocker", or "no_response") — if you produce a real result, deliver it with send_message or it is lost.'}`;
607
+ notifyHint = `\n\nIf your task result is worth notifying the user about, send it proactively via send_message to platform="${normalizedConfig.notifyPlatform}" to="${normalizedConfig.notifyTo}" and set purpose="final_result" for a concrete useful outcome or purpose="blocker" for a real issue the user should know about. If nothing important or actionable changed, call send_message with purpose="no_response" and content="[NO RESPONSE]" exactly; never leave content blank for no_response. When a tool result already gives you summary fields or flags that answer the task, decide from that evidence instead of re-running nearby variants of the same lookup.${manualRun ? '' : ' For this automatic scheduled run, plain assistant text is internal only and is NOT delivered. You MUST end the run with exactly one explicit send_message decision (purpose="final_result", "blocker", or "no_response") — if you produce a real result, deliver it with send_message or it is lost.'}`;
493
608
  }
494
609
 
495
610
  const triggerPayloadText = executionMeta.triggerPayload
@@ -514,20 +629,22 @@ class TaskRuntime {
514
629
  const finalPrompt = basePrompt + recoveryNote;
515
630
  const runOptions = {
516
631
  triggerType: task.trigger_type || 'schedule',
517
- triggerSource: task.trigger_type || 'schedule',
632
+ triggerSource: executionMeta.triggerSource || task.trigger_type || 'schedule',
518
633
  agentId,
519
634
  app: this.app,
520
635
  conversationId,
521
636
  taskId,
637
+ bypassUserRateLimits: true,
522
638
  deliveryState,
523
639
  allowMultipleProactiveMessages: normalizedConfig.allowMultipleMessages === true || normalizedConfig.allow_multiple_messages === true,
640
+ stageProactiveMessages: true,
524
641
  skipTaskAnalysis: true,
525
642
  skipDeliverableWorkflow: true,
526
643
  skipGlobalRecall: true,
527
644
  skipConversationHistory: true,
528
645
  skipConversationMaintenance: true,
529
646
  skipRunContextPersistence: true,
530
- skipVerifier: true,
647
+ skipVerifier: false,
531
648
  stream: false,
532
649
  context: executionMeta.triggerPayload || {},
533
650
  };
@@ -563,6 +680,16 @@ class TaskRuntime {
563
680
  );
564
681
  }
565
682
  this.io.to(`user:${userId}`).emit('tasks:task_complete', { taskId, result });
683
+ this._recordTaskLifecycle({
684
+ userId,
685
+ taskId,
686
+ taskName,
687
+ agentId,
688
+ eventKind: 'task_completed',
689
+ runId: completedRunId,
690
+ triggerType: executionMeta.triggerType || null,
691
+ triggerSource: executionMeta.triggerSource || null,
692
+ });
566
693
  return result;
567
694
  } catch (err) {
568
695
  const transientExecutionError = isTransientError(err);
@@ -571,6 +698,18 @@ class TaskRuntime {
571
698
  }
572
699
  if (err?.code === 'TASK_DELIVERY_FAILED') throw err;
573
700
  if (transientExecutionError) {
701
+ this._recordTaskLifecycle({
702
+ userId,
703
+ taskId,
704
+ taskName,
705
+ agentId,
706
+ eventKind: 'task_skipped',
707
+ runId: completedRunId,
708
+ reason: 'transient_rate_limit',
709
+ error: err.message,
710
+ triggerType: executionMeta.triggerType || null,
711
+ triggerSource: executionMeta.triggerSource || null,
712
+ });
574
713
  this.io.to(`user:${userId}`).emit('tasks:task_skipped', {
575
714
  taskId,
576
715
  reason: 'transient_rate_limit',
@@ -620,10 +759,79 @@ class TaskRuntime {
620
759
  error: err.message,
621
760
  timestamp: new Date().toISOString(),
622
761
  });
762
+ this._recordTaskLifecycle({
763
+ userId,
764
+ taskId,
765
+ taskName,
766
+ agentId,
767
+ eventKind: 'task_failed',
768
+ runId: completedRunId,
769
+ error: err.message,
770
+ triggerType: executionMeta.triggerType || null,
771
+ triggerSource: executionMeta.triggerSource || null,
772
+ });
623
773
  return { skipped: false, error: err.message, runId: completedRunId };
624
774
  }
625
775
  }
626
776
 
777
+ _recordTaskLifecycle({
778
+ userId,
779
+ taskId,
780
+ taskName,
781
+ agentId = null,
782
+ eventKind,
783
+ runId = null,
784
+ reason = null,
785
+ error = null,
786
+ triggerType = null,
787
+ triggerSource = null,
788
+ }) {
789
+ if (!this.timelineService?.recordTaskLifecycle) {
790
+ return;
791
+ }
792
+ this.timelineService.recordTaskLifecycle({
793
+ userId,
794
+ agentId,
795
+ taskId,
796
+ taskName,
797
+ eventKind,
798
+ runId,
799
+ reason,
800
+ error,
801
+ triggerType,
802
+ triggerSource,
803
+ });
804
+ }
805
+
806
+ _evaluateTaskLoopBudget(task, taskConfig, userId, options = {}) {
807
+ const budget = normalizeLoopBudgetConfig(taskConfig);
808
+ const usage = this.taskRepository.getTaskLoopUsageToday(task.id, userId);
809
+ const runCount = Number(usage.runCount || 0);
810
+ const totalTokens = Number(usage.totalTokens || 0);
811
+ const snapshot = {
812
+ enabled: budget.enabled,
813
+ paused: budget.paused,
814
+ runCount,
815
+ totalTokens,
816
+ maxRunsPerDay: budget.maxRunsPerDay,
817
+ maxTokensPerDay: budget.maxTokensPerDay,
818
+ };
819
+
820
+ if (!budget.enabled) {
821
+ return { mode: 'normal', reason: null, snapshot };
822
+ }
823
+ if (budget.paused) {
824
+ return { mode: 'paused', reason: 'loop_budget_paused', snapshot };
825
+ }
826
+
827
+ const projectedRunCount = options.manual === true ? runCount : runCount + 1;
828
+ if (projectedRunCount > budget.maxRunsPerDay || totalTokens >= budget.maxTokensPerDay) {
829
+ return { mode: 'exhausted', reason: 'loop_budget_exhausted', snapshot };
830
+ }
831
+
832
+ return { mode: 'normal', reason: null, snapshot };
833
+ }
834
+
627
835
  _normalizeJson(value) {
628
836
  return normalizeJsonObject(value);
629
837
  }
@@ -716,6 +924,7 @@ class TaskRuntime {
716
924
  lastTriggeredAt: row.last_triggered_at || null,
717
925
  taskType: row.task_type || 'agent_prompt',
718
926
  taskConfig,
927
+ loopBudget: normalizeLoopBudgetConfig(taskConfig),
719
928
  prompt: taskConfig.prompt || '',
720
929
  model: taskConfig.model || null,
721
930
  agentId,
@@ -841,13 +1050,19 @@ class TaskRuntime {
841
1050
  if (!targets.length) return null;
842
1051
  const resultText = stringifyTaskResult(result).trim();
843
1052
  const resultLooksLikeError = Boolean(result?.error);
1053
+ const stagedMessage = normalizeOutgoingMessageForPlatform(
1054
+ deliveryState?.stagedProactiveMessage?.platform,
1055
+ deliveryState?.stagedProactiveMessage?.content || '',
1056
+ { stripNoResponseMarker: false },
1057
+ );
1058
+ const explicitStagedDelivery = deliveryState?.proactiveMessageStaged === true && Boolean(stagedMessage);
844
1059
  // A forced terminal wrap-up (read-only/blocked hard-stop) is the model's final
845
1060
  // answer produced WITHOUT the ability to call send_message itself. Gating it
846
1061
  // would silently drop a stuck scheduled task's result, so deliver it even on an
847
1062
  // automatic run. Ordinary mid-run plain text (model had send_message available
848
1063
  // and chose not to use it) is still gated below.
849
1064
  const forcedTerminal = deliveryState?.terminalWrapup === true && Boolean(resultText);
850
- if (!allowPlainResultFallback && !resultLooksLikeError && !forcedTerminal) {
1065
+ if (!allowPlainResultFallback && !resultLooksLikeError && !forcedTerminal && !explicitStagedDelivery) {
851
1066
  // Automatic run produced substantive text but never made an explicit
852
1067
  // send_message decision (a deliberate no_response would have short-circuited
853
1068
  // above). We suppress to avoid recurring-check spam, but surface it so a
@@ -874,10 +1089,17 @@ class TaskRuntime {
874
1089
  }
875
1090
 
876
1091
  let lastError = null;
877
- for (const target of targets) {
1092
+ const resolvedTargets = explicitStagedDelivery
1093
+ ? [{
1094
+ platform: deliveryState.stagedProactiveMessage.platform,
1095
+ to: deliveryState.stagedProactiveMessage.to,
1096
+ mediaPath: deliveryState.stagedProactiveMessage.mediaPath || null,
1097
+ }]
1098
+ : targets;
1099
+ for (const target of resolvedTargets) {
878
1100
  const message = normalizeOutgoingMessageForPlatform(
879
1101
  target.platform,
880
- resultText,
1102
+ resultText || stagedMessage,
881
1103
  { stripNoResponseMarker: false },
882
1104
  );
883
1105
  if (!message || message.toUpperCase() === '[NO RESPONSE]') return null;
@@ -893,10 +1115,13 @@ class TaskRuntime {
893
1115
  try {
894
1116
  const sendResult = await manager.sendMessage(userId, target.platform, target.to, message, {
895
1117
  agentId,
1118
+ mediaPath: target.mediaPath || null,
896
1119
  runId: result?.runId || null,
897
1120
  persistConversation: true,
898
1121
  });
899
1122
  deliveryState.messagingSent = true;
1123
+ deliveryState.proactiveMessageStaged = false;
1124
+ deliveryState.stagedProactiveMessage = null;
900
1125
  deliveryState.lastSentMessage = message;
901
1126
  if (!Array.isArray(deliveryState.sentMessages)) {
902
1127
  deliveryState.sentMessages = [];
@@ -144,6 +144,19 @@ class TaskRepository {
144
144
  db.prepare('UPDATE scheduled_tasks SET last_run = datetime(\'now\') WHERE id = ? AND user_id = ?').run(taskId, userId);
145
145
  }
146
146
 
147
+ getTaskLoopUsageToday(taskId, userId) {
148
+ return db.prepare(
149
+ `SELECT
150
+ COUNT(*) AS runCount,
151
+ COALESCE(SUM(total_tokens), 0) AS totalTokens
152
+ FROM agent_runs
153
+ WHERE user_id = ?
154
+ AND json_valid(metadata_json)
155
+ AND CAST(json_extract(metadata_json, '$.taskId') AS TEXT) = CAST(? AS TEXT)
156
+ AND date(created_at) = date('now')`
157
+ ).get(userId, String(taskId)) || { runCount: 0, totalTokens: 0 };
158
+ }
159
+
147
160
  markAgentRunFailed(runId, userId, error) {
148
161
  db.prepare(
149
162
  `UPDATE agent_runs