neoagent 3.0.1-beta.8 → 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.
- package/.env.example +2 -10
- package/README.md +12 -3
- package/docs/automation.md +37 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +34 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +10 -8
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +38 -13
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1612 -214
- package/flutter_app/lib/main_controller.dart +398 -26
- package/flutter_app/lib/main_devices.dart +293 -207
- package/flutter_app/lib/main_models.dart +142 -0
- package/flutter_app/lib/main_navigation.dart +19 -1
- package/flutter_app/lib/main_operations.dart +288 -9
- package/flutter_app/lib/main_settings.dart +510 -300
- package/flutter_app/lib/main_shared.dart +2 -0
- package/flutter_app/lib/main_timeline.dart +1378 -0
- package/flutter_app/lib/src/backend_client.dart +149 -19
- package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
- package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
- package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
- package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
- package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
- package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
- package/flutter_app/lib/src/recording_payloads.dart +9 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
- package/flutter_app/windows/runner/flutter_window.cpp +75 -0
- package/landing/index.html +3 -3
- package/lib/manager.js +184 -66
- package/package.json +4 -1
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +436 -10
- package/server/admin/billing.js +165 -42
- package/server/admin/index.html +80 -5
- package/server/admin/users.js +15 -15
- package/server/db/database.js +125 -20
- package/server/http/routes.js +1 -0
- package/server/http/static.js +4 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.js.symbols +8475 -8467
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +91077 -87037
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +345 -64
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +5 -0
- package/server/routes/browser.js +8 -1
- package/server/routes/recordings.js +96 -6
- package/server/routes/screenHistory.js +140 -2
- package/server/routes/timeline.js +43 -0
- package/server/services/account/erasure.js +263 -0
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/agent_engine_core.js +8 -1
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/conversation_loop.js +166 -33
- package/server/services/ai/messagingFallback.js +22 -0
- package/server/services/ai/rate_limits.js +28 -5
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/ai/tools.js +163 -4
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/plans.js +2 -1
- package/server/services/browser/anti_detection.js +192 -0
- package/server/services/browser/controller.js +180 -54
- package/server/services/desktop/auth.js +3 -0
- package/server/services/desktop/registry.js +50 -2
- package/server/services/integrations/google/calendar.js +22 -14
- package/server/services/manager.js +12 -42
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/messaging/telnyx.js +9 -8
- package/server/services/recordings/manager.js +60 -27
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +188 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/social_video/service.js +60 -10
- package/server/services/tasks/runtime.js +234 -9
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/tasks/task_repository.js +13 -0
- package/server/services/timeline/service.js +558 -0
- package/server/services/wearable/gateway.js +1 -1
- package/server/services/websocket.js +21 -3
- package/server/services/desktop/screenRecorder.js +0 -292
- 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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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:
|
|
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
|
-
|
|
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 = [];
|
|
@@ -141,11 +141,11 @@ function parseCronExpression(expression) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function matchesCron(date, schedule) {
|
|
144
|
-
const minute = date.
|
|
145
|
-
const hour = date.
|
|
146
|
-
const dayOfMonth = date.
|
|
147
|
-
const month = date.
|
|
148
|
-
const dayOfWeek = date.
|
|
144
|
+
const minute = date.getMinutes();
|
|
145
|
+
const hour = date.getHours();
|
|
146
|
+
const dayOfMonth = date.getDate();
|
|
147
|
+
const month = date.getMonth() + 1;
|
|
148
|
+
const dayOfWeek = date.getDay();
|
|
149
149
|
|
|
150
150
|
if (!schedule.minute.values.has(minute)) return false;
|
|
151
151
|
if (!schedule.hour.values.has(hour)) return false;
|
|
@@ -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
|