neoagent 2.4.1-beta.9 → 2.4.1

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 (234) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +36 -4
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -3
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +13 -0
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +2 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +197 -42
  52. package/flutter_app/lib/main_devices.dart +436 -4
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +673 -871
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +46 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
  70. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  71. package/flutter_app/lib/src/stream_renderer.dart +42 -3
  72. package/flutter_app/lib/src/theme/palette.dart +76 -34
  73. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  74. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  75. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  81. package/flutter_app/pubspec.lock +2 -2
  82. package/flutter_app/pubspec.yaml +7 -1
  83. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  84. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  85. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  94. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  95. package/flutter_app/web/favicon.png +0 -0
  96. package/flutter_app/web/favicon.svg +40 -9
  97. package/flutter_app/web/favicon_light.svg +43 -0
  98. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  99. package/flutter_app/web/icons/Icon-192.png +0 -0
  100. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-512.png +0 -0
  102. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  106. package/flutter_app/windows/runner/main.cpp +7 -1
  107. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  108. package/lib/manager.js +445 -83
  109. package/package.json +17 -3
  110. package/runtime/paths.js +3 -1
  111. package/server/admin/access.js +198 -0
  112. package/server/admin/admin.css +268 -0
  113. package/server/admin/admin.js +348 -0
  114. package/server/admin/analytics.js +128 -0
  115. package/server/admin/index.html +1015 -0
  116. package/server/admin/login.html +290 -0
  117. package/server/admin/logo.svg +43 -0
  118. package/server/admin/sql.js +134 -0
  119. package/server/admin/users.js +147 -0
  120. package/server/config/origins.js +3 -1
  121. package/server/db/database.js +92 -0
  122. package/server/db/ftsQuery.js +27 -0
  123. package/server/http/routes.js +1 -0
  124. package/server/http/static.js +23 -6
  125. package/server/index.js +1 -1
  126. package/server/middleware/adminAuth.js +48 -0
  127. package/server/middleware/auth.js +1 -40
  128. package/server/public/.last_build_id +1 -1
  129. package/server/public/app_icon.png +0 -0
  130. package/server/public/assets/AssetManifest.bin +1 -1
  131. package/server/public/assets/AssetManifest.bin.json +1 -1
  132. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  133. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  134. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  138. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  139. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  140. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  141. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  142. package/server/public/assets/web/icons/Icon-192.png +0 -0
  143. package/server/public/favicon.png +0 -0
  144. package/server/public/favicon.svg +40 -9
  145. package/server/public/favicon_light.svg +43 -0
  146. package/server/public/flutter_bootstrap.js +2 -2
  147. package/server/public/icons/Icon-192-light.png +0 -0
  148. package/server/public/icons/Icon-192.png +0 -0
  149. package/server/public/icons/Icon-512-light.png +0 -0
  150. package/server/public/icons/Icon-512.png +0 -0
  151. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  152. package/server/public/icons/Icon-maskable-192.png +0 -0
  153. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-512.png +0 -0
  155. package/server/public/main.dart.js +88537 -85936
  156. package/server/routes/admin.js +632 -0
  157. package/server/routes/agent_profiles.js +3 -0
  158. package/server/routes/agents.js +1 -1
  159. package/server/routes/auth.js +3 -1
  160. package/server/routes/mcp.js +29 -13
  161. package/server/routes/memory.js +23 -1
  162. package/server/routes/messaging.js +2 -0
  163. package/server/routes/screenHistory.js +14 -8
  164. package/server/routes/settings.js +10 -2
  165. package/server/routes/stream.js +1 -1
  166. package/server/routes/triggers.js +4 -4
  167. package/server/routes/voice_assistant.js +36 -1
  168. package/server/routes/workspace.js +86 -0
  169. package/server/services/account/admin_two_factor.js +132 -0
  170. package/server/services/account/password_policy.js +6 -1
  171. package/server/services/ai/completion.js +44 -0
  172. package/server/services/ai/engine.js +321 -500
  173. package/server/services/ai/imageAnalysis.js +9 -5
  174. package/server/services/ai/logFormat.js +46 -0
  175. package/server/services/ai/loopPolicy.js +11 -0
  176. package/server/services/ai/messagingFallback.js +228 -0
  177. package/server/services/ai/models.js +194 -55
  178. package/server/services/ai/providerRetry.js +169 -0
  179. package/server/services/ai/providers/anthropic.js +15 -4
  180. package/server/services/ai/providers/google.js +38 -11
  181. package/server/services/ai/providers/grok.js +19 -68
  182. package/server/services/ai/providers/grokOauth.js +141 -0
  183. package/server/services/ai/providers/nvidia.js +154 -0
  184. package/server/services/ai/providers/ollama.js +76 -39
  185. package/server/services/ai/providers/openai.js +20 -31
  186. package/server/services/ai/providers/openaiCodex.js +6 -2
  187. package/server/services/ai/providers/openaiCompatible.js +70 -0
  188. package/server/services/ai/providers/openrouter.js +162 -0
  189. package/server/services/ai/settings.js +30 -0
  190. package/server/services/ai/systemPrompt.js +51 -29
  191. package/server/services/ai/taskAnalysis.js +60 -1
  192. package/server/services/ai/toolEvidence.js +207 -0
  193. package/server/services/ai/toolSelector.js +8 -1
  194. package/server/services/ai/tools.js +78 -13
  195. package/server/services/browser/extension/registry.js +94 -9
  196. package/server/services/desktop/registry.js +104 -1
  197. package/server/services/desktop/screenRecorder.js +208 -98
  198. package/server/services/desktop/screen_recorder_support.js +46 -0
  199. package/server/services/integrations/google/provider.js +13 -0
  200. package/server/services/integrations/home_assistant/constants.js +88 -0
  201. package/server/services/integrations/home_assistant/network.js +207 -0
  202. package/server/services/integrations/home_assistant/provider.js +249 -0
  203. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  204. package/server/services/integrations/home_assistant/tools.js +101 -0
  205. package/server/services/integrations/manager.js +5 -0
  206. package/server/services/integrations/registry.js +2 -0
  207. package/server/services/integrations/trello/provider.js +8 -2
  208. package/server/services/manager.js +51 -53
  209. package/server/services/mcp/client.js +208 -268
  210. package/server/services/mcp/client_support.js +172 -0
  211. package/server/services/mcp/recovery.js +116 -0
  212. package/server/services/mcp/tool_operations.js +123 -0
  213. package/server/services/memory/ingestion.js +185 -370
  214. package/server/services/memory/ingestion_coverage.js +129 -0
  215. package/server/services/memory/ingestion_documents.js +123 -0
  216. package/server/services/memory/ingestion_support.js +171 -0
  217. package/server/services/memory/intelligence.js +181 -0
  218. package/server/services/memory/manager.js +476 -32
  219. package/server/services/messaging/automation.js +71 -134
  220. package/server/services/messaging/formatting_guides.js +26 -1
  221. package/server/services/messaging/inbound_queue.js +111 -0
  222. package/server/services/messaging/manager.js +25 -8
  223. package/server/services/messaging/typing_keepalive.js +110 -0
  224. package/server/services/streaming/android-stream.js +293 -40
  225. package/server/services/tasks/integration_runtime.js +4 -1
  226. package/server/services/tasks/runtime.js +415 -56
  227. package/server/services/tasks/task_repository.js +48 -6
  228. package/server/services/websocket.js +8 -2
  229. package/server/services/widgets/service.js +17 -1
  230. package/server/services/workspace/manager.js +116 -0
  231. package/server/utils/logger.js +44 -6
  232. package/server/utils/security.js +3 -0
  233. package/server/utils/version.js +29 -19
  234. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -13,36 +13,125 @@ const { TaskRepository } = require('./task_repository');
13
13
  const { TriggerRegistry } = require('./trigger_registry');
14
14
  const scheduleAdapter = require('./adapters/schedule');
15
15
  const { normalizeJsonObject } = require('./utils');
16
+ const { normalizeOutgoingMessageForPlatform } = require('../messaging/formatting_guides');
16
17
 
17
18
  const MAX_AUTONOMOUS_RETRIES = 1;
18
19
  const MAX_RECURRING_TASK_START_DELAY_MS = 90 * 1000;
19
20
  const INTEGRATION_TRIGGER_POLL_CRON = '* * * * *';
20
21
 
22
+ function normalizeStoredString(value) {
23
+ if (value == null) return '';
24
+ if (typeof value !== 'string') return String(value || '').trim();
25
+ let current = value.trim();
26
+ for (let i = 0; i < 2; i += 1) {
27
+ if (!current) return '';
28
+ try {
29
+ const parsed = JSON.parse(current);
30
+ if (typeof parsed === 'string') {
31
+ current = parsed.trim();
32
+ continue;
33
+ }
34
+ return '';
35
+ } catch {
36
+ return current;
37
+ }
38
+ }
39
+ return current;
40
+ }
41
+
42
+ function normalizeNotifyTarget(target = {}) {
43
+ const platform = normalizeStoredString(target.platform);
44
+ const to = normalizeStoredString(target.to);
45
+ if (!platform || !to) return null;
46
+ return { platform, to };
47
+ }
48
+
49
+ function stringifyTaskResult(result) {
50
+ if (typeof result === 'string') return result;
51
+ if (result == null) return '';
52
+ if (typeof result !== 'object') return String(result);
53
+
54
+ for (const key of ['content', 'message', 'text', 'summary', 'finalResponse', 'final_response']) {
55
+ if (typeof result[key] === 'string' && result[key].trim()) {
56
+ return result[key];
57
+ }
58
+ }
59
+
60
+ if (result.result != null && result.result !== result) {
61
+ const nested = stringifyTaskResult(result.result);
62
+ if (nested) return nested;
63
+ }
64
+
65
+ return '';
66
+ }
67
+
21
68
  class TaskRuntime {
22
- constructor(io, agentEngine, app = null) {
69
+ constructor(io, agentEngine, app = null, options = {}) {
23
70
  this.io = io;
24
71
  this.agentEngine = agentEngine;
25
72
  this.app = app;
73
+ this.cron = options.cron || cron;
26
74
  this.taskRepository = new TaskRepository();
27
75
  this.scheduleJobs = new Map();
28
76
  this.runningTaskExecutions = new Set();
77
+ this.activeExecutionPromises = new Set();
78
+ this.activePolls = new Map();
29
79
  this.integrationEventCleanups = [];
30
80
  this.triggerRegistry = new TriggerRegistry(taskAdapters);
81
+ this.started = false;
82
+ this.stopping = false;
83
+ this.stopPromise = null;
84
+ this.state = 'idle';
85
+ this.lastError = null;
31
86
  }
32
87
 
33
88
  get integrationManager() {
34
89
  return this.app?.locals?.integrationManager || null;
35
90
  }
36
91
 
92
+ getStatus() {
93
+ return {
94
+ state: this.state,
95
+ started: this.started,
96
+ stopping: this.stopping,
97
+ scheduledJobCount: this.scheduleJobs.size,
98
+ activeExecutionCount: this.activeExecutionPromises.size,
99
+ activePolls: Array.from(this.activePolls.keys()),
100
+ lastError: this.lastError,
101
+ };
102
+ }
103
+
37
104
  start() {
38
- this._loadFromDB();
39
- this._startOneTimePoller();
40
- this._startIntegrationPoller();
41
- this.integrationEventCleanups = attachIntegrationEventSources(this);
42
- console.log('[Tasks] Started');
105
+ if (this.started) {
106
+ return this.getStatus();
107
+ }
108
+ if (this.stopPromise) {
109
+ throw new Error('Task runtime cannot start while shutdown is in progress.');
110
+ }
111
+
112
+ this.started = true;
113
+ this.stopping = false;
114
+ this.state = 'starting';
115
+ this.lastError = null;
116
+ try {
117
+ this._loadFromDB();
118
+ this._startOneTimePoller();
119
+ this._startIntegrationPoller();
120
+ this.integrationEventCleanups = attachIntegrationEventSources(this);
121
+ this.state = 'running';
122
+ console.log('[Tasks] Started');
123
+ return this.getStatus();
124
+ } catch (error) {
125
+ this.lastError = error.message;
126
+ this.started = false;
127
+ this.stopping = false;
128
+ this.state = 'error';
129
+ this._stopScheduling();
130
+ throw error;
131
+ }
43
132
  }
44
133
 
45
- stop() {
134
+ _stopScheduling() {
46
135
  for (const [, job] of this.scheduleJobs) {
47
136
  job.task.stop();
48
137
  }
@@ -51,12 +140,51 @@ class TaskRuntime {
51
140
  if (poller) poller.stop();
52
141
  }
53
142
  for (const cleanup of this.integrationEventCleanups) {
54
- cleanup();
143
+ try {
144
+ cleanup();
145
+ } catch (error) {
146
+ console.error('[Tasks] Event source cleanup failed:', error.message);
147
+ }
55
148
  }
56
149
  this.integrationEventCleanups = [];
57
150
  this.oneTimePoller = null;
58
151
  this.integrationPoller = null;
59
- console.log('[Tasks] Stopped');
152
+ }
153
+
154
+ async stop() {
155
+ if (this.stopPromise) {
156
+ return this.stopPromise;
157
+ }
158
+ if (
159
+ !this.started
160
+ && this.state === 'stopped'
161
+ && this.scheduleJobs.size === 0
162
+ && this.activePolls.size === 0
163
+ && this.activeExecutionPromises.size === 0
164
+ ) {
165
+ return this.getStatus();
166
+ }
167
+
168
+ this.started = false;
169
+ this.stopping = true;
170
+ this.state = 'stopping';
171
+ this._stopScheduling();
172
+
173
+ this.stopPromise = (async () => {
174
+ while (this.activePolls.size > 0 || this.activeExecutionPromises.size > 0) {
175
+ await Promise.allSettled([
176
+ ...this.activePolls.values(),
177
+ ...this.activeExecutionPromises,
178
+ ]);
179
+ }
180
+ this.stopping = false;
181
+ this.state = 'stopped';
182
+ console.log('[Tasks] Stopped');
183
+ return this.getStatus();
184
+ })().finally(() => {
185
+ this.stopPromise = null;
186
+ });
187
+ return this.stopPromise;
60
188
  }
61
189
 
62
190
  getTriggerCatalog(userId, options = {}) {
@@ -138,6 +266,8 @@ class TaskRuntime {
138
266
  manual: true,
139
267
  triggerType: task.trigger_type || 'schedule',
140
268
  triggerSource: 'manual',
269
+ }).catch((error) => {
270
+ console.error(`[Tasks] Manual task ${taskId} error:`, error.message);
141
271
  });
142
272
  return { running: true };
143
273
  }
@@ -152,9 +282,8 @@ class TaskRuntime {
152
282
  if (String(task.last_trigger_fingerprint || '') === fingerprint) {
153
283
  return { skipped: true, reason: 'duplicate_trigger' };
154
284
  }
155
- this.taskRepository.markTaskTriggered(taskId, userId, fingerprint);
156
285
 
157
- return this._executeTask(taskId, userId, {
286
+ const result = await this._executeTask(taskId, userId, {
158
287
  manual: false,
159
288
  oneTime: false,
160
289
  scheduledAt: triggerPayload.timestamp || new Date().toISOString(),
@@ -162,58 +291,106 @@ class TaskRuntime {
162
291
  triggerSource: task.trigger_type || 'schedule',
163
292
  triggerPayload: triggerPayload.context || {},
164
293
  });
294
+ if (!result?.error && !result?.skipped) {
295
+ this.taskRepository.markTaskTriggered(taskId, userId, fingerprint);
296
+ }
297
+ return result;
165
298
  }
166
299
 
167
300
  _startOneTimePoller() {
168
- this.oneTimePoller = cron.schedule('* * * * *', async () => {
169
- const due = this.taskRepository.listDueOneTimeTasks();
301
+ this.oneTimePoller = this.cron.schedule('* * * * *', () => {
302
+ return this._runPoll('one_time', () => this._runDueOneTimeTasks(), (error) => {
303
+ console.error('[Tasks] One-time task poll failed:', error.message);
304
+ });
305
+ });
306
+ }
170
307
 
171
- for (const task of due) {
172
- this.scheduleJobs.delete(task.id);
173
- try {
174
- await this._executeTask(task.id, task.user_id, {
175
- scheduledAt: task.run_at || new Date().toISOString(),
176
- oneTime: true,
177
- triggerType: 'schedule',
178
- triggerSource: 'schedule',
179
- });
180
- } catch (err) {
181
- console.error(`[Tasks] One-time task ${task.id} error:`, err.message);
308
+ async _runDueOneTimeTasks() {
309
+ const due = this.taskRepository.listDueOneTimeTasks();
310
+
311
+ for (const task of due) {
312
+ this.scheduleJobs.delete(task.id);
313
+ try {
314
+ const result = await this._executeTask(task.id, task.user_id, {
315
+ scheduledAt: task.run_at || new Date().toISOString(),
316
+ oneTime: true,
317
+ triggerType: 'schedule',
318
+ triggerSource: 'schedule',
319
+ });
320
+ if (result?.skipped) {
321
+ continue;
182
322
  }
183
- this.taskRepository.deleteById(task.id, task.user_id);
323
+ this.taskRepository.deleteTask(task.id, task.user_id);
184
324
  this.io.to(`user:${task.user_id}`).emit('tasks:task_deleted', { taskId: task.id });
325
+ } catch (err) {
326
+ console.error(`[Tasks] One-time task ${task.id} error:`, err.message);
185
327
  }
186
- });
328
+ }
187
329
  }
188
330
 
189
331
  _startIntegrationPoller() {
190
- this.integrationPoller = cron.schedule(INTEGRATION_TRIGGER_POLL_CRON, async () => {
191
- const tasks = this.taskRepository.listEnabledByTriggerTypes(POLLED_TRIGGER_TYPES);
192
- for (const task of tasks) {
193
- try {
194
- await pollIntegrationTask(this, task);
195
- } catch (error) {
196
- console.error(`[Tasks] Trigger poll failed for task ${task.id}:`, error.message);
332
+ this.integrationPoller = this.cron.schedule(INTEGRATION_TRIGGER_POLL_CRON, () => {
333
+ return this._runPoll('integration', async () => {
334
+ const tasks = this.taskRepository.listEnabledByTriggerTypes(POLLED_TRIGGER_TYPES);
335
+ for (const task of tasks) {
336
+ try {
337
+ await pollIntegrationTask(this, task);
338
+ } catch (error) {
339
+ console.error(`[Tasks] Trigger poll failed for task ${task.id}:`, error.message);
340
+ }
197
341
  }
198
- }
342
+ }, (error) => {
343
+ console.error('[Tasks] Integration trigger poll failed:', error.message);
344
+ });
199
345
  });
200
346
  }
201
347
 
348
+ _runPoll(name, callback, onError) {
349
+ const active = this.activePolls.get(name);
350
+ if (active) {
351
+ return active;
352
+ }
353
+ if (this.stopping) {
354
+ return Promise.resolve({ skipped: true, reason: 'runtime_stopping' });
355
+ }
356
+
357
+ const promise = Promise.resolve()
358
+ .then(callback)
359
+ .catch((error) => {
360
+ this.lastError = error.message;
361
+ onError(error);
362
+ return { error: error.message };
363
+ })
364
+ .finally(() => {
365
+ if (this.activePolls.get(name) === promise) {
366
+ this.activePolls.delete(name);
367
+ }
368
+ });
369
+ this.activePolls.set(name, promise);
370
+ return promise;
371
+ }
372
+
202
373
  async _registerTask(task) {
203
- if (!task || !task.enabled) return;
374
+ if (!task) return;
375
+ this._unregisterTask(task.id);
376
+ if (!task.enabled) return;
204
377
  if ((task.trigger_type || 'schedule') !== 'schedule') return;
205
378
  const triggerConfig = this._normalizeJson(task.trigger_config);
206
379
  if (triggerConfig.mode === 'one_time') return;
207
380
  const cronExpression = String(triggerConfig.cronExpression || '').trim();
208
381
  if (!cronExpression) return;
209
- const job = cron.schedule(cronExpression, async () => {
210
- await this._executeTask(task.id, task.user_id, {
211
- scheduledAt: new Date().toISOString(),
212
- manual: false,
213
- oneTime: false,
214
- triggerType: 'schedule',
215
- triggerSource: 'schedule',
216
- });
382
+ const job = this.cron.schedule(cronExpression, async () => {
383
+ try {
384
+ await this._executeTask(task.id, task.user_id, {
385
+ scheduledAt: new Date().toISOString(),
386
+ manual: false,
387
+ oneTime: false,
388
+ triggerType: 'schedule',
389
+ triggerSource: 'schedule',
390
+ });
391
+ } catch (error) {
392
+ console.error(`[Tasks] Scheduled task ${task.id} error:`, error.message);
393
+ }
217
394
  });
218
395
  this.scheduleJobs.set(task.id, { task: job, userId: task.user_id });
219
396
  }
@@ -227,6 +404,9 @@ class TaskRuntime {
227
404
  }
228
405
 
229
406
  async _executeTask(taskId, userId, executionMeta = {}) {
407
+ if (this.stopping) {
408
+ return { skipped: true, reason: 'runtime_stopping' };
409
+ }
230
410
  const executionKey = `${userId}:${taskId}`;
231
411
  if (this.runningTaskExecutions.has(executionKey)) {
232
412
  this.io.to(`user:${userId}`).emit('tasks:task_skipped', {
@@ -238,10 +418,13 @@ class TaskRuntime {
238
418
  }
239
419
 
240
420
  this.runningTaskExecutions.add(executionKey);
421
+ const executionPromise = this._executeTaskSerial(taskId, userId, executionMeta);
422
+ this.activeExecutionPromises.add(executionPromise);
241
423
  try {
242
- return await this._executeTaskSerial(taskId, userId, executionMeta);
424
+ return await executionPromise;
243
425
  } finally {
244
426
  this.runningTaskExecutions.delete(executionKey);
427
+ this.activeExecutionPromises.delete(executionPromise);
245
428
  }
246
429
  }
247
430
 
@@ -273,6 +456,15 @@ class TaskRuntime {
273
456
  this.taskRepository.markTaskRun(taskId, userId);
274
457
  this.io.to(`user:${userId}`).emit('tasks:task_running', { taskId, timestamp: new Date().toISOString() });
275
458
 
459
+ let normalizedConfig = taskConfig;
460
+ const taskName = task.name || `Task ${taskId}`;
461
+ const deliveryState = {
462
+ messagingSent: false,
463
+ noResponse: false,
464
+ lastSentMessage: '',
465
+ sentMessages: [],
466
+ };
467
+ let completedRunId = null;
276
468
  try {
277
469
  if (task.task_type === 'widget_refresh') {
278
470
  const widgetService = this.app?.locals?.widgetService;
@@ -288,8 +480,7 @@ class TaskRuntime {
288
480
  return result;
289
481
  }
290
482
 
291
- const normalizedConfig = this._ensureDefaultNotifyTarget(userId, agentId, taskConfig, taskId);
292
- const taskName = task.name || `Task ${taskId}`;
483
+ normalizedConfig = this._ensureDefaultNotifyTarget(userId, agentId, taskConfig, taskId);
293
484
  const triggerSummary = this._summarizeTrigger(task.trigger_type, triggerConfig);
294
485
  let notifyHint = '';
295
486
 
@@ -315,7 +506,6 @@ class TaskRuntime {
315
506
  ].filter(Boolean).join('\n\n');
316
507
 
317
508
  const conversationId = this._getTaskConversation(userId, taskId, taskName, agentId);
318
- const deliveryState = { messagingSent: false, lastSentMessage: '', sentMessages: [] };
319
509
  let attempt = 0;
320
510
  let recoveryNote = '';
321
511
  while (attempt <= MAX_AUTONOMOUS_RETRIES) {
@@ -330,6 +520,7 @@ class TaskRuntime {
330
520
  deliveryState,
331
521
  allowMultipleProactiveMessages: normalizedConfig.allowMultipleMessages === true || normalizedConfig.allow_multiple_messages === true,
332
522
  skipTaskAnalysis: true,
523
+ skipDeliverableWorkflow: true,
333
524
  skipGlobalRecall: true,
334
525
  skipConversationHistory: true,
335
526
  skipConversationMaintenance: true,
@@ -342,11 +533,42 @@ class TaskRuntime {
342
533
  const result = typeof this.agentEngine.runWithModel === 'function'
343
534
  ? await this.agentEngine.runWithModel(userId, finalPrompt, runOptions, normalizedConfig.model || null)
344
535
  : await this.agentEngine.run(userId, finalPrompt, runOptions);
536
+ completedRunId = result?.runId || null;
537
+ const fallbackDelivery = await this._deliverTaskResultIfNeeded({
538
+ userId,
539
+ agentId,
540
+ taskId,
541
+ taskConfig: normalizedConfig,
542
+ result,
543
+ deliveryState,
544
+ });
545
+ if (fallbackDelivery && result && typeof result === 'object') {
546
+ result.taskDelivery = fallbackDelivery;
547
+ }
548
+ if (fallbackDelivery?.error) {
549
+ const deliveryError = new Error(fallbackDelivery.error);
550
+ deliveryError.code = 'TASK_DELIVERY_FAILED';
551
+ throw deliveryError;
552
+ }
553
+ if (
554
+ !deliveryState.messagingSent
555
+ && !deliveryState.noResponse
556
+ && !stringifyTaskResult(result).trim()
557
+ ) {
558
+ throw new Error(
559
+ 'Background task completed without producing a result or an explicit no-response decision.',
560
+ );
561
+ }
345
562
  this.io.to(`user:${userId}`).emit('tasks:task_complete', { taskId, result });
346
563
  return result;
347
564
  } catch (err) {
565
+ if (completedRunId) {
566
+ this.taskRepository.markAgentRunFailed(completedRunId, userId, err.message);
567
+ }
568
+ if (err?.code === 'TASK_DELIVERY_FAILED') throw err;
348
569
  if (attempt >= MAX_AUTONOMOUS_RETRIES) throw err;
349
570
  attempt += 1;
571
+ completedRunId = null;
350
572
  recoveryNote = [
351
573
  '\n\n[SYSTEM: Previous task attempt failed]',
352
574
  `Error: ${String(err?.message || 'Unknown runtime error')}`,
@@ -361,8 +583,24 @@ class TaskRuntime {
361
583
  }
362
584
  } catch (err) {
363
585
  console.error(`[Tasks] Task ${taskId} error:`, err.message);
364
- this.io.to(`user:${userId}`).emit('tasks:task_error', { taskId, error: err.message });
365
- return { skipped: false, error: err.message };
586
+ if (err?.code !== 'TASK_DELIVERY_FAILED') {
587
+ await this._deliverTaskResultIfNeeded({
588
+ userId,
589
+ agentId,
590
+ taskId,
591
+ taskConfig: normalizedConfig,
592
+ result: {
593
+ content: `Background task "${taskName}" could not complete after retrying. Check the task run logs for details.`,
594
+ },
595
+ deliveryState,
596
+ });
597
+ }
598
+ this.io.to(`user:${userId}`).emit('tasks:task_skipped', {
599
+ taskId,
600
+ reason: 'execution_failed',
601
+ timestamp: new Date().toISOString(),
602
+ });
603
+ return { skipped: false, error: err.message, runId: completedRunId };
366
604
  }
367
605
  }
368
606
 
@@ -451,7 +689,10 @@ class TaskRuntime {
451
689
  triggerSummary,
452
690
  nextRun: triggerType === 'schedule' ? scheduleAdapter.nextRun(triggerConfig) : null,
453
691
  enabled: !!row.enabled,
454
- lastRun: row.last_run || null,
692
+ lastRun: row.last_run_started_at || row.last_run || null,
693
+ lastRunId: row.last_run_id || null,
694
+ lastRunStatus: row.last_run_status || null,
695
+ lastRunError: row.last_run_error || null,
455
696
  lastTriggeredAt: row.last_triggered_at || null,
456
697
  taskType: row.task_type || 'agent_prompt',
457
698
  taskConfig,
@@ -485,25 +726,143 @@ class TaskRuntime {
485
726
 
486
727
  _getDefaultNotifyTarget(userId, agentId = null) {
487
728
  const scopedAgentId = resolveAgentId(userId, agentId);
488
- return {
729
+ return normalizeNotifyTarget({
489
730
  platform: this._getAgentSetting(userId, scopedAgentId, 'last_platform'),
490
731
  to: this._getAgentSetting(userId, scopedAgentId, 'last_chat_id'),
491
- };
732
+ });
733
+ }
734
+
735
+ _buildNotifyTargets(userId, agentId, taskConfig = {}) {
736
+ const scopedAgentId = resolveAgentId(userId, agentId);
737
+ const candidates = [
738
+ normalizeNotifyTarget({
739
+ platform: taskConfig.notifyPlatform,
740
+ to: taskConfig.notifyTo,
741
+ }),
742
+ this._getDefaultNotifyTarget(userId, scopedAgentId),
743
+ ...this.taskRepository.listRecentMessageTargets(userId, scopedAgentId).map((row) => normalizeNotifyTarget({
744
+ platform: row.platform,
745
+ to: row.platform_chat_id,
746
+ })),
747
+ ];
748
+
749
+ const unique = [];
750
+ const seen = new Set();
751
+ for (const target of candidates) {
752
+ if (!target) continue;
753
+ const key = `${target.platform}:${target.to}`;
754
+ if (seen.has(key)) continue;
755
+ seen.add(key);
756
+ unique.push(target);
757
+ }
758
+ return unique;
492
759
  }
493
760
 
494
761
  _ensureDefaultNotifyTarget(userId, agentId, taskConfig, taskId) {
495
762
  const normalized = { ...taskConfig };
496
- if (!normalized.callTo && (!normalized.notifyPlatform || !normalized.notifyTo)) {
497
- const notifyTarget = this._getDefaultNotifyTarget(userId, agentId);
498
- if (notifyTarget.platform && notifyTarget.to) {
763
+ const existingTarget = normalizeNotifyTarget({
764
+ platform: normalized.notifyPlatform,
765
+ to: normalized.notifyTo,
766
+ });
767
+ if (existingTarget) {
768
+ normalized.notifyPlatform = existingTarget.platform;
769
+ normalized.notifyTo = existingTarget.to;
770
+ }
771
+ if (!normalized.callTo && !existingTarget) {
772
+ const notifyTarget = this._buildNotifyTargets(userId, agentId, normalized)[0];
773
+ if (notifyTarget) {
499
774
  normalized.notifyPlatform = notifyTarget.platform;
500
775
  normalized.notifyTo = notifyTarget.to;
501
- this.taskRepository.updateTaskConfig(taskId, userId, normalized);
502
776
  }
503
777
  }
778
+ if (
779
+ normalized.notifyPlatform !== taskConfig.notifyPlatform
780
+ || normalized.notifyTo !== taskConfig.notifyTo
781
+ ) {
782
+ this.taskRepository.updateTaskConfig(taskId, userId, normalized);
783
+ }
504
784
  return normalized;
505
785
  }
506
786
 
787
+ async _deliverTaskResultIfNeeded({
788
+ userId,
789
+ agentId,
790
+ taskId,
791
+ taskConfig,
792
+ result,
793
+ deliveryState,
794
+ }) {
795
+ if (deliveryState?.messagingSent || deliveryState?.noResponse || taskConfig.callTo) return null;
796
+ const targets = this._buildNotifyTargets(userId, agentId, taskConfig);
797
+ if (!targets.length) return null;
798
+
799
+ const manager = this.app?.locals?.messagingManager || this.agentEngine?.messagingManager || null;
800
+ if (!manager) {
801
+ return {
802
+ sent: false,
803
+ error: 'Messaging delivery is unavailable on this server.',
804
+ };
805
+ }
806
+
807
+ let lastError = null;
808
+ for (const target of targets) {
809
+ const message = normalizeOutgoingMessageForPlatform(
810
+ target.platform,
811
+ stringifyTaskResult(result),
812
+ { stripNoResponseMarker: false },
813
+ );
814
+ if (!message || message.toUpperCase() === '[NO RESPONSE]') return null;
815
+
816
+ const status = typeof manager.getPlatformStatus === 'function'
817
+ ? manager.getPlatformStatus(userId, target.platform, { agentId })
818
+ : null;
819
+ if (!status || status.status !== 'connected') {
820
+ lastError = new Error(`Platform ${target.platform} is not connected on this server.`);
821
+ continue;
822
+ }
823
+
824
+ try {
825
+ const sendResult = await manager.sendMessage(userId, target.platform, target.to, message, {
826
+ agentId,
827
+ runId: result?.runId || null,
828
+ persistConversation: true,
829
+ });
830
+ deliveryState.messagingSent = true;
831
+ deliveryState.lastSentMessage = message;
832
+ if (!Array.isArray(deliveryState.sentMessages)) {
833
+ deliveryState.sentMessages = [];
834
+ }
835
+ deliveryState.sentMessages.push(message);
836
+
837
+ if (taskConfig.notifyPlatform !== target.platform || taskConfig.notifyTo !== target.to) {
838
+ this.taskRepository.updateTaskConfig(taskId, userId, {
839
+ ...taskConfig,
840
+ notifyPlatform: target.platform,
841
+ notifyTo: target.to,
842
+ });
843
+ }
844
+
845
+ return {
846
+ sent: true,
847
+ platform: target.platform,
848
+ to: target.to,
849
+ result: sendResult,
850
+ };
851
+ } catch (error) {
852
+ lastError = error;
853
+ }
854
+ }
855
+
856
+ if (lastError) {
857
+ console.error(`[Tasks] Task ${taskId} notification delivery failed:`, lastError.message);
858
+ return {
859
+ sent: false,
860
+ error: lastError.message,
861
+ };
862
+ }
863
+ return null;
864
+ }
865
+
507
866
  _getTaskConversation(userId, taskId, taskName, agentId = null) {
508
867
  const scopedAgentId = resolveAgentId(userId, agentId);
509
868
  const platform = 'tasks';