neoagent 2.4.1-beta.33 → 2.4.1-beta.35

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 (31) hide show
  1. package/.env.example +10 -3
  2. package/flutter_app/lib/main_admin.dart +13 -0
  3. package/flutter_app/lib/main_controller.dart +24 -6
  4. package/flutter_app/lib/main_models.dart +28 -0
  5. package/flutter_app/lib/main_operations.dart +117 -0
  6. package/package.json +1 -1
  7. package/server/public/.last_build_id +1 -1
  8. package/server/public/flutter_bootstrap.js +1 -1
  9. package/server/public/main.dart.js +37961 -37794
  10. package/server/routes/mcp.js +29 -13
  11. package/server/routes/memory.js +1 -0
  12. package/server/services/ai/engine.js +64 -10
  13. package/server/services/ai/systemPrompt.js +28 -22
  14. package/server/services/ai/tools.js +24 -3
  15. package/server/services/desktop/screenRecorder.js +208 -98
  16. package/server/services/desktop/screen_recorder_support.js +46 -0
  17. package/server/services/manager.js +51 -53
  18. package/server/services/mcp/client.js +208 -268
  19. package/server/services/mcp/client_support.js +172 -0
  20. package/server/services/mcp/recovery.js +116 -0
  21. package/server/services/mcp/tool_operations.js +123 -0
  22. package/server/services/memory/ingestion.js +185 -370
  23. package/server/services/memory/ingestion_coverage.js +129 -0
  24. package/server/services/memory/ingestion_documents.js +123 -0
  25. package/server/services/memory/ingestion_support.js +171 -0
  26. package/server/services/messaging/automation.js +71 -134
  27. package/server/services/messaging/inbound_queue.js +111 -0
  28. package/server/services/messaging/typing_keepalive.js +110 -0
  29. package/server/services/tasks/integration_runtime.js +4 -1
  30. package/server/services/tasks/runtime.js +240 -53
  31. package/server/services/tasks/task_repository.js +35 -6
package/.env.example CHANGED
@@ -219,9 +219,16 @@ DEEPGRAM_LANGUAGE=multi
219
219
 
220
220
  OLLAMA_URL=http://localhost:11434
221
221
 
222
- # Local screen OCR capture (macOS desktop context recorder).
223
- # Set to false to disable background screen capture/OCR polling.
224
- NEOAGENT_SCREEN_RECORDER_ENABLED=true
222
+ # Connected integration memory refresh cadence in milliseconds.
223
+ NEOAGENT_MEMORY_INGESTION_INTERVAL_MS=600000
224
+
225
+ # Local screen OCR capture. This captures the backend host's macOS desktop,
226
+ # so enable it only when that host is the configured user's desktop.
227
+ NEOAGENT_SCREEN_RECORDER_ENABLED=false
228
+ # Existing NeoAgent user ID that owns all captured screen history. Required when enabled.
229
+ NEOAGENT_SCREEN_RECORDER_USER_ID=
230
+ NEOAGENT_SCREEN_RECORDER_INTERVAL_MS=10000
231
+ NEOAGENT_SCREEN_RECORDER_RETENTION_DAYS=7
225
232
 
226
233
  ########################################
227
234
  # Messaging and voice integrations
@@ -429,6 +429,8 @@ class McpPanel extends StatelessWidget {
429
429
  label: server.status,
430
430
  color: server.status == 'running'
431
431
  ? _success
432
+ : server.hasError
433
+ ? _danger
432
434
  : _textSecondary,
433
435
  ),
434
436
  ],
@@ -465,6 +467,17 @@ class McpPanel extends StatelessWidget {
465
467
  ),
466
468
  ],
467
469
  ),
470
+ if (server.hasError) ...<Widget>[
471
+ const SizedBox(height: 12),
472
+ _InlineError(message: server.error!),
473
+ if (server.retryLabel.isNotEmpty) ...<Widget>[
474
+ const SizedBox(height: 8),
475
+ Text(
476
+ server.retryLabel,
477
+ style: TextStyle(color: _textSecondary),
478
+ ),
479
+ ],
480
+ ],
468
481
  const SizedBox(height: 14),
469
482
  Wrap(
470
483
  spacing: 10,
@@ -6112,18 +6112,36 @@ class NeoAgentController extends ChangeNotifier {
6112
6112
  }
6113
6113
 
6114
6114
  Future<void> startMcpServer(int id) async {
6115
- await _backendClient.startMcpServer(backendUrl, id);
6116
- await refreshMcp();
6115
+ try {
6116
+ await _backendClient.startMcpServer(backendUrl, id);
6117
+ await refreshMcp();
6118
+ } catch (error) {
6119
+ errorMessage = _friendlyErrorMessage(error);
6120
+ await refreshMcp();
6121
+ notifyListeners();
6122
+ }
6117
6123
  }
6118
6124
 
6119
6125
  Future<void> stopMcpServer(int id) async {
6120
- await _backendClient.stopMcpServer(backendUrl, id);
6121
- await refreshMcp();
6126
+ try {
6127
+ await _backendClient.stopMcpServer(backendUrl, id);
6128
+ await refreshMcp();
6129
+ } catch (error) {
6130
+ errorMessage = _friendlyErrorMessage(error);
6131
+ await refreshMcp();
6132
+ notifyListeners();
6133
+ }
6122
6134
  }
6123
6135
 
6124
6136
  Future<void> deleteMcpServer(int id) async {
6125
- await _backendClient.deleteMcpServer(backendUrl, id);
6126
- await refreshMcp();
6137
+ try {
6138
+ await _backendClient.deleteMcpServer(backendUrl, id);
6139
+ await refreshMcp();
6140
+ } catch (error) {
6141
+ errorMessage = _friendlyErrorMessage(error);
6142
+ await refreshMcp();
6143
+ notifyListeners();
6144
+ }
6127
6145
  }
6128
6146
 
6129
6147
  Future<void> requestHealthPermissions() async {
@@ -3099,6 +3099,9 @@ class TaskItem {
3099
3099
  required this.model,
3100
3100
  required this.enabled,
3101
3101
  required this.lastRun,
3102
+ required this.lastRunId,
3103
+ required this.lastRunStatus,
3104
+ required this.lastRunError,
3102
3105
  required this.taskType,
3103
3106
  required this.widgetId,
3104
3107
  });
@@ -3144,6 +3147,9 @@ class TaskItem {
3144
3147
  '',
3145
3148
  enabled: json['enabled'] != false,
3146
3149
  lastRun: _parseOptionalTimestamp(json['lastRun']?.toString()),
3150
+ lastRunId: json['lastRunId']?.toString() ?? '',
3151
+ lastRunStatus: json['lastRunStatus']?.toString() ?? '',
3152
+ lastRunError: json['lastRunError']?.toString() ?? '',
3147
3153
  taskType:
3148
3154
  json['taskType']?.toString().ifEmpty(
3149
3155
  json['task_type']?.toString() ?? 'agent_prompt',
@@ -3168,12 +3174,20 @@ class TaskItem {
3168
3174
  final String model;
3169
3175
  final bool enabled;
3170
3176
  final DateTime? lastRun;
3177
+ final String lastRunId;
3178
+ final String lastRunStatus;
3179
+ final String lastRunError;
3171
3180
  final String taskType;
3172
3181
  final String widgetId;
3173
3182
 
3174
3183
  String get scheduleLabel =>
3175
3184
  triggerSummary.trim().isEmpty ? 'Task trigger' : triggerSummary;
3176
3185
  String get lastRunLabel => lastRun == null ? '' : _formatTimestamp(lastRun!);
3186
+ String get lastRunStatusLabel =>
3187
+ _titleCase(lastRunStatus.replaceAll('_', ' '));
3188
+ bool get hasLastRunStatus => lastRunStatus.trim().isNotEmpty;
3189
+ bool get lastRunFailed =>
3190
+ lastRunStatus == 'failed' || lastRunStatus == 'error';
3177
3191
  bool get hasModelOverride => model.trim().isNotEmpty;
3178
3192
  bool get isWidgetRefresh => taskType == 'widget_refresh';
3179
3193
  }
@@ -3383,6 +3397,9 @@ class McpServerItem {
3383
3397
  required this.enabled,
3384
3398
  required this.status,
3385
3399
  required this.toolCount,
3400
+ required this.error,
3401
+ required this.consecutiveFails,
3402
+ required this.nextRetryAt,
3386
3403
  });
3387
3404
 
3388
3405
  factory McpServerItem.fromJson(Map<dynamic, dynamic> json) {
@@ -3397,6 +3414,9 @@ class McpServerItem {
3397
3414
  enabled: json['enabled'] == true,
3398
3415
  status: json['status']?.toString().ifEmpty('stopped') ?? 'stopped',
3399
3416
  toolCount: _asInt(json['toolCount']),
3417
+ error: json['error']?.toString(),
3418
+ consecutiveFails: _asInt(json['consecutiveFails']),
3419
+ nextRetryAt: _parseOptionalTimestamp(json['nextRetryAt']?.toString()),
3400
3420
  );
3401
3421
  }
3402
3422
 
@@ -3408,6 +3428,14 @@ class McpServerItem {
3408
3428
  final bool enabled;
3409
3429
  final String status;
3410
3430
  final int toolCount;
3431
+ final String? error;
3432
+ final int consecutiveFails;
3433
+ final DateTime? nextRetryAt;
3434
+
3435
+ bool get hasError => (error ?? '').trim().isNotEmpty;
3436
+ String get retryLabel => nextRetryAt == null
3437
+ ? ''
3438
+ : 'Next retry: ${_formatTimestamp(nextRetryAt!)}';
3411
3439
 
3412
3440
  String get authMethodLabel {
3413
3441
  final auth = _jsonMap(config['auth']);
@@ -4555,6 +4555,89 @@ class _TasksPanelState extends State<TasksPanel> {
4555
4555
 
4556
4556
  NeoAgentController get controller => widget.controller;
4557
4557
 
4558
+ Color _taskRunStatusColor(String status) {
4559
+ switch (status) {
4560
+ case 'completed':
4561
+ return _success;
4562
+ case 'failed':
4563
+ case 'error':
4564
+ return _danger;
4565
+ case 'running':
4566
+ case 'retrying':
4567
+ return _warning;
4568
+ default:
4569
+ return _textSecondary;
4570
+ }
4571
+ }
4572
+
4573
+ Future<void> _showLastRun(TaskItem task) async {
4574
+ final runId = task.lastRunId.trim();
4575
+ if (runId.isEmpty) return;
4576
+
4577
+ try {
4578
+ final detail = await controller.fetchRunDetail(runId, force: true);
4579
+ if (!mounted) return;
4580
+ await showDialog<void>(
4581
+ context: context,
4582
+ builder: (dialogContext) => AlertDialog(
4583
+ title: Text(detail.run.title),
4584
+ content: ConstrainedBox(
4585
+ constraints: const BoxConstraints(maxWidth: 620),
4586
+ child: SingleChildScrollView(
4587
+ child: Column(
4588
+ mainAxisSize: MainAxisSize.min,
4589
+ crossAxisAlignment: CrossAxisAlignment.start,
4590
+ children: <Widget>[
4591
+ Wrap(
4592
+ spacing: 8,
4593
+ runSpacing: 8,
4594
+ children: <Widget>[
4595
+ _StatusPill(
4596
+ label: detail.run.statusLabel,
4597
+ color: detail.run.statusColor,
4598
+ ),
4599
+ _StatusPill(
4600
+ label: '${detail.steps.length} steps',
4601
+ color: _textSecondary,
4602
+ ),
4603
+ ],
4604
+ ),
4605
+ if (detail.run.error.trim().isNotEmpty) ...<Widget>[
4606
+ const SizedBox(height: 16),
4607
+ Text(detail.run.error, style: TextStyle(color: _danger)),
4608
+ ],
4609
+ if (detail.response.trim().isNotEmpty) ...<Widget>[
4610
+ const SizedBox(height: 16),
4611
+ SelectableText(detail.response),
4612
+ ],
4613
+ if (detail.response.trim().isEmpty &&
4614
+ detail.run.error.trim().isEmpty) ...<Widget>[
4615
+ const SizedBox(height: 16),
4616
+ Text(
4617
+ 'This run did not produce a user-facing response.',
4618
+ style: TextStyle(color: _textSecondary),
4619
+ ),
4620
+ ],
4621
+ ],
4622
+ ),
4623
+ ),
4624
+ ),
4625
+ actions: <Widget>[
4626
+ TextButton(
4627
+ onPressed: () => Navigator.of(dialogContext).pop(),
4628
+ child: const Text('Close'),
4629
+ ),
4630
+ ],
4631
+ ),
4632
+ );
4633
+ } catch (error) {
4634
+ if (!mounted) return;
4635
+ ScaffoldMessenger.of(context).showSnackBar(
4636
+ SnackBar(content: Text(controller.friendlyErrorMessage(error))),
4637
+ );
4638
+ }
4639
+ }
4640
+
4558
4641
  @override
4559
4642
  void didUpdateWidget(covariant TasksPanel oldWidget) {
4560
4643
  super.didUpdateWidget(oldWidget);
@@ -4703,6 +4786,13 @@ class _TasksPanelState extends State<TasksPanel> {
4703
4786
  label: task.enabled ? 'Active' : 'Paused',
4704
4787
  color: task.enabled ? _success : _textSecondary,
4705
4788
  ),
4789
+ if (task.hasLastRunStatus) ...<Widget>[
4790
+ const SizedBox(width: 8),
4791
+ _StatusPill(
4792
+ label: 'Last: ${task.lastRunStatusLabel}',
4793
+ color: _taskRunStatusColor(task.lastRunStatus),
4794
+ ),
4795
+ ],
4706
4796
  ],
4707
4797
  ),
4708
4798
  const SizedBox(height: 10),
@@ -4734,6 +4824,11 @@ class _TasksPanelState extends State<TasksPanel> {
4734
4824
  style: TextStyle(color: _textSecondary),
4735
4825
  ),
4736
4826
  ],
4827
+ if (task.lastRunFailed &&
4828
+ task.lastRunError.trim().isNotEmpty) ...<Widget>[
4829
+ const SizedBox(height: 8),
4830
+ Text(task.lastRunError, style: TextStyle(color: _danger)),
4831
+ ],
4737
4832
  const SizedBox(height: 14),
4738
4833
  Wrap(
4739
4834
  spacing: 10,
@@ -4753,6 +4848,11 @@ class _TasksPanelState extends State<TasksPanel> {
4753
4848
  : () => controller.runTaskNow(task.id),
4754
4849
  child: Text(_manualRunButtonLabel('Run Now', remaining)),
4755
4850
  ),
4851
+ if (task.lastRunId.trim().isNotEmpty)
4852
+ OutlinedButton(
4853
+ onPressed: () => _showLastRun(task),
4854
+ child: const Text('View last run'),
4855
+ ),
4756
4856
  OutlinedButton(
4757
4857
  onPressed: () => _confirmDelete(
4758
4858
  context,
@@ -4805,6 +4905,13 @@ class _TasksPanelState extends State<TasksPanel> {
4805
4905
  label: task.enabled ? 'Active' : 'Paused',
4806
4906
  color: task.enabled ? _success : _textSecondary,
4807
4907
  ),
4908
+ if (task.hasLastRunStatus) ...<Widget>[
4909
+ const SizedBox(width: 8),
4910
+ _StatusPill(
4911
+ label: 'Last: ${task.lastRunStatusLabel}',
4912
+ color: _taskRunStatusColor(task.lastRunStatus),
4913
+ ),
4914
+ ],
4808
4915
  ],
4809
4916
  ),
4810
4917
  const SizedBox(height: 10),
@@ -4839,6 +4946,11 @@ class _TasksPanelState extends State<TasksPanel> {
4839
4946
  style: TextStyle(color: _textSecondary),
4840
4947
  ),
4841
4948
  ],
4949
+ if (task.lastRunFailed &&
4950
+ task.lastRunError.trim().isNotEmpty) ...<Widget>[
4951
+ const SizedBox(height: 8),
4952
+ Text(task.lastRunError, style: TextStyle(color: _danger)),
4953
+ ],
4842
4954
  const SizedBox(height: 14),
4843
4955
  Wrap(
4844
4956
  spacing: 10,
@@ -4868,6 +4980,11 @@ class _TasksPanelState extends State<TasksPanel> {
4868
4980
  _manualRunButtonLabel('Refresh Now', remaining),
4869
4981
  ),
4870
4982
  ),
4983
+ if (task.lastRunId.trim().isNotEmpty)
4984
+ OutlinedButton(
4985
+ onPressed: () => _showLastRun(task),
4986
+ child: const Text('View last run'),
4987
+ ),
4871
4988
  OutlinedButton(
4872
4989
  onPressed: linkedWidget == null
4873
4990
  ? null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.1-beta.33",
3
+ "version": "2.4.1-beta.35",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- 49442e542f5b85df16e4fff2751cb6a3
1
+ ddef1ad4fea366bdfef04805d0a9528b
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"c416acfeb8126e097f758c664aaa3da929e27d
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "2270904145" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "1175396021" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });