neoagent 2.2.1-beta.6 → 2.2.1-beta.7

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 (49) hide show
  1. package/docs/automation.md +2 -2
  2. package/docs/capabilities.md +7 -10
  3. package/docs/hardware.md +4 -7
  4. package/docs/index.md +6 -7
  5. package/docs/integrations.md +1 -1
  6. package/docs/operations.md +1 -1
  7. package/docs/why-neoagent.md +2 -2
  8. package/package.json +1 -1
  9. package/server/db/database.js +76 -61
  10. package/server/http/routes.js +1 -2
  11. package/server/public/assets/AssetManifest.json +1 -1
  12. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  13. package/server/public/flutter_bootstrap.js +1 -1
  14. package/server/public/main.dart.js +65118 -64805
  15. package/server/routes/{scheduler.js → tasks.js} +31 -29
  16. package/server/routes/widgets.js +7 -7
  17. package/server/services/ai/capabilityHealth.js +4 -4
  18. package/server/services/ai/engine.js +9 -9
  19. package/server/services/ai/systemPrompt.js +7 -7
  20. package/server/services/ai/taskAnalysis.js +3 -3
  21. package/server/services/ai/toolResult.js +6 -8
  22. package/server/services/ai/tools.js +62 -95
  23. package/server/services/commands/router.js +14 -6
  24. package/server/services/integrations/whatsapp/provider.js +23 -1
  25. package/server/services/manager.js +14 -14
  26. package/server/services/memory/manager.js +7 -7
  27. package/server/services/memory/policy.js +1 -1
  28. package/server/services/messaging/formatting_guides.js +0 -4
  29. package/server/services/messaging/manager.js +0 -2
  30. package/server/services/tasks/adapters/gmail_message_received.js +36 -0
  31. package/server/services/tasks/adapters/index.js +10 -0
  32. package/server/services/tasks/adapters/outlook_email_received.js +38 -0
  33. package/server/services/tasks/adapters/schedule.js +57 -0
  34. package/server/services/tasks/adapters/slack_message_received.js +39 -0
  35. package/server/services/tasks/adapters/teams_message_received.js +39 -0
  36. package/server/services/tasks/adapters/whatsapp_personal_message_received.js +42 -0
  37. package/server/services/tasks/integration_runtime.js +260 -0
  38. package/server/services/tasks/runtime.js +539 -0
  39. package/server/services/{scheduler/cron_utils.js → tasks/schedule_utils.js} +2 -0
  40. package/server/services/tasks/security.js +60 -0
  41. package/server/services/tasks/task_repository.js +162 -0
  42. package/server/services/tasks/trigger_registry.js +29 -0
  43. package/server/services/tasks/utils.js +45 -0
  44. package/server/services/websocket.js +1 -1
  45. package/server/services/widgets/service.js +37 -25
  46. package/server/routes/wearable_device.js +0 -147
  47. package/server/services/messaging/waveshare_wearable.js +0 -40
  48. package/server/services/scheduler/cron.js +0 -580
  49. package/server/services/wearables/device_auth.js +0 -228
@@ -92,7 +92,7 @@ function compactToolDefinition(tool, options = {}) {
92
92
  }
93
93
 
94
94
  function isProactiveTrigger(triggerSource) {
95
- return triggerSource === 'scheduler';
95
+ return triggerSource === 'schedule' || triggerSource === 'tasks';
96
96
  }
97
97
 
98
98
  function getRunState(engine, runId) {
@@ -559,7 +559,7 @@ function getAvailableTools(app, options = {}) {
559
559
  },
560
560
  {
561
561
  name: 'memory_save',
562
- description: 'Save ONE specific, self-contained fact to long-term semantic memory. RULES: (1) One discrete fact per call — if you have 10 facts, call this 10 times. (2) The ENTIRE value must be IN the content string itself — never write a pointer/reference like "user shared a profile" or "see chat history for details". That is useless. (3) Content must be a complete statement a stranger could read cold and understand. (4) Only save durable facts, preferences, or stable project context — never save recent scheduler runs, task statuses, execution receipts, or other transient operational logs. GOOD: "XYZ lives in" / "XYZ prefers dark mode". BAD: "User pasted a profile dump" / "XYZ shared lots of details — see chat history" / "XYZ gave a big list of projects" / "Recent scheduler run: backup completed".',
562
+ description: 'Save ONE specific, self-contained fact to long-term semantic memory. RULES: (1) One discrete fact per call — if you have 10 facts, call this 10 times. (2) The ENTIRE value must be IN the content string itself — never write a pointer/reference like "user shared a profile" or "see chat history for details". That is useless. (3) Content must be a complete statement a stranger could read cold and understand. (4) Only save durable facts, preferences, or stable project context — never save recent task runs, task statuses, execution receipts, or other transient operational logs. GOOD: "XYZ lives in" / "XYZ prefers dark mode". BAD: "User pasted a profile dump" / "XYZ shared lots of details — see chat history" / "XYZ gave a big list of projects" / "Recent task run: backup completed".',
563
563
  parameters: {
564
564
  type: 'object',
565
565
  properties: {
@@ -870,68 +870,54 @@ function getAvailableTools(app, options = {}) {
870
870
  }
871
871
  },
872
872
  {
873
- name: 'create_scheduled_task',
874
- description: 'Create a RECURRING scheduled task (cron job). Use this for repeating automations — daily reminders, weekly checks, etc. For a one-time future run, use schedule_run instead.',
873
+ name: 'create_task',
874
+ description: 'Create a background task with a named trigger and self-contained prompt.',
875
875
  parameters: {
876
876
  type: 'object',
877
877
  properties: {
878
- name: { type: 'string', description: 'Short descriptive name for the task' },
879
- cron_expression: { type: 'string', description: 'Cron expression for the schedule, e.g. "0 9 * * 1-5" for weekdays at 9am, "*/30 * * * *" for every 30 minutes. Use standard 5-field cron syntax.' },
880
- prompt: { type: 'string', description: 'The prompt/instructions the agent will run when triggered. Be specific about what to do and who to notify.' },
881
- enabled: { type: 'boolean', description: 'Whether to activate immediately (default true)' },
882
- model: { type: 'string', description: 'Optional specific AI model ID to force for this task. Omit to use the normal automatic/default model selection.' },
883
- call_to: { type: 'string', description: 'E.164 phone number to call via Telnyx when this task fires, e.g. "+12125550100".' },
884
- call_greeting: { type: 'string', description: 'Opening sentence spoken to the user when the call is answered. Required if call_to is set.' }
878
+ name: { type: 'string', description: 'Short descriptive name for the task.' },
879
+ trigger_type: { type: 'string', description: 'Trigger type such as schedule, gmail_message_received, outlook_email_received, slack_message_received, teams_message_received, or whatsapp_personal_message_received.' },
880
+ trigger_config: { type: 'object', description: 'Trigger-specific configuration object.' },
881
+ prompt: { type: 'string', description: 'The instructions the agent will run when the trigger fires.' },
882
+ enabled: { type: 'boolean', description: 'Whether to activate immediately.' },
883
+ model: { type: 'string', description: 'Optional model override.' },
884
+ call_to: { type: 'string', description: 'Optional E.164 phone number to call via Telnyx when this task fires.' },
885
+ call_greeting: { type: 'string', description: 'Optional spoken greeting hint for make_call.' }
885
886
  },
886
- required: ['name', 'cron_expression', 'prompt']
887
+ required: ['name', 'trigger_type', 'trigger_config', 'prompt']
887
888
  }
888
889
  },
889
890
  {
890
- name: 'schedule_run',
891
- description: 'Schedule a ONE-TIME agent run at a specific future datetime. The run fires once, then is automatically deleted. Use this for reminders, delayed tasks, or anything the user wants done at a specific time. Accepts any ISO 8601 datetime string.',
892
- parameters: {
893
- type: 'object',
894
- properties: {
895
- name: { type: 'string', description: 'Short descriptive name, e.g. "Remind about meeting"' },
896
- run_at: { type: 'string', description: 'ISO 8601 datetime when the run should fire, e.g. "2026-03-09T22:00:00"' },
897
- prompt: { type: 'string', description: 'The prompt/instructions the agent will execute at that time. Be specific.' },
898
- model: { type: 'string', description: 'Optional specific AI model ID to force for this run. Omit to use the normal automatic/default model selection.' },
899
- call_to: { type: 'string', description: 'Optional E.164 phone number to call via Telnyx when this fires.' },
900
- call_greeting: { type: 'string', description: 'Opening sentence spoken when the Telnyx call is answered.' }
901
- },
902
- required: ['name', 'run_at', 'prompt']
903
- }
904
- },
905
- {
906
- name: 'list_scheduled_tasks',
907
- description: 'List all scheduled tasks/cron jobs for this user.',
891
+ name: 'list_tasks',
892
+ description: 'List all tasks for this user and agent.',
908
893
  parameters: { type: 'object', properties: {} }
909
894
  },
910
895
  {
911
- name: 'delete_scheduled_task',
912
- description: 'Delete a scheduled task by its ID.',
896
+ name: 'delete_task',
897
+ description: 'Delete a task by its ID.',
913
898
  parameters: {
914
899
  type: 'object',
915
900
  properties: {
916
- task_id: { type: 'number', description: 'The numeric ID of the task to delete (get it from list_scheduled_tasks)' }
901
+ task_id: { type: 'number', description: 'The numeric ID of the task to delete.' }
917
902
  },
918
903
  required: ['task_id']
919
904
  }
920
905
  },
921
906
  {
922
- name: 'update_scheduled_task',
923
- description: 'Update an existing scheduled task change its name, schedule, prompt, enabled state, or Telnyx call settings.',
907
+ name: 'update_task',
908
+ description: 'Update an existing task, including its trigger, prompt, or enabled state.',
924
909
  parameters: {
925
910
  type: 'object',
926
911
  properties: {
927
- task_id: { type: 'number', description: 'The numeric ID of the task to update (get it from list_scheduled_tasks)' },
928
- name: { type: 'string', description: 'New name for the task' },
929
- cron_expression: { type: 'string', description: 'New cron expression, e.g. "0 8 * * *" for daily at 8am' },
930
- prompt: { type: 'string', description: 'New prompt/instructions for the task' },
931
- enabled: { type: 'boolean', description: 'Enable or disable the task' },
932
- model: { type: 'string', description: 'Specific AI model ID for this task. Set to empty string to clear the override and go back to automatic/default selection.' },
933
- call_to: { type: 'string', description: 'E.164 phone number to call via Telnyx when this task fires. Set to empty string to remove.' },
934
- call_greeting: { type: 'string', description: 'New opening sentence spoken when the Telnyx call is answered.' }
912
+ task_id: { type: 'number', description: 'The numeric ID of the task to update.' },
913
+ name: { type: 'string', description: 'New name for the task.' },
914
+ trigger_type: { type: 'string', description: 'Updated trigger type.' },
915
+ trigger_config: { type: 'object', description: 'Updated trigger-specific configuration.' },
916
+ prompt: { type: 'string', description: 'Updated task prompt.' },
917
+ enabled: { type: 'boolean', description: 'Enable or disable the task.' },
918
+ model: { type: 'string', description: 'Specific AI model ID for this task. Set to empty string to clear the override.' },
919
+ call_to: { type: 'string', description: 'Optional E.164 phone number to call via Telnyx when this task fires. Set to empty string to remove.' },
920
+ call_greeting: { type: 'string', description: 'Updated spoken greeting hint.' }
935
921
  },
936
922
  required: ['task_id']
937
923
  }
@@ -1151,7 +1137,7 @@ function getAvailableTools(app, options = {}) {
1151
1137
  tools.push(...integrationTools);
1152
1138
  }
1153
1139
 
1154
- if (options.triggerSource === 'scheduler' && options.widgetId) {
1140
+ if ((options.triggerSource === 'schedule' || options.triggerSource === 'tasks') && options.widgetId) {
1155
1141
  tools.push({
1156
1142
  name: 'save_widget_snapshot',
1157
1143
  description: 'Save the refreshed structured snapshot for the widget that is currently being updated. Call this exactly once per widget refresh run.',
@@ -1246,7 +1232,7 @@ async function executeTool(toolName, args, context, engine) {
1246
1232
  const mcp = () => app?.locals?.mcpManager || app?.locals?.mcpClient || engine.mcpManager;
1247
1233
  const integrations = () => app?.locals?.integrationManager || null;
1248
1234
  const sk = () => app?.locals?.skillRunner || engine.skillRunner;
1249
- const sched = () => app?.locals?.scheduler || engine.scheduler;
1235
+ const taskRuntime = () => app?.locals?.taskRuntime || engine.taskRuntime;
1250
1236
  const rec = () => app?.locals?.recordingManager || null;
1251
1237
  const widgets = () => app?.locals?.widgetService || null;
1252
1238
 
@@ -1837,7 +1823,7 @@ async function executeTool(toolName, args, context, engine) {
1837
1823
  return {
1838
1824
  called: false,
1839
1825
  skipped: true,
1840
- reason: 'A proactive notification was already sent in this scheduler run; duplicate make_call was suppressed.'
1826
+ reason: 'A proactive notification was already sent in this task run; duplicate make_call was suppressed.'
1841
1827
  };
1842
1828
  }
1843
1829
 
@@ -1896,7 +1882,7 @@ async function executeTool(toolName, args, context, engine) {
1896
1882
  return {
1897
1883
  sent: false,
1898
1884
  skipped: true,
1899
- reason: 'A proactive message was already sent in this scheduler run; duplicate send_message was suppressed.'
1885
+ reason: 'A proactive message was already sent in this task run; duplicate send_message was suppressed.'
1900
1886
  };
1901
1887
  }
1902
1888
 
@@ -1904,7 +1890,7 @@ async function executeTool(toolName, args, context, engine) {
1904
1890
  agentId,
1905
1891
  mediaPath: args.media_path,
1906
1892
  runId,
1907
- persistConversation: triggerSource === 'scheduler'
1893
+ persistConversation: triggerSource === 'schedule' || triggerSource === 'tasks'
1908
1894
  });
1909
1895
  // Track that the agent explicitly sent a message during this run
1910
1896
  if (!suppressReply && sendResult?.suppressed !== true) {
@@ -2104,7 +2090,7 @@ async function executeTool(toolName, args, context, engine) {
2104
2090
  const message = typeof args.message === 'string' ? args.message.trim() : '';
2105
2091
  if (!message) return { error: 'message is required' };
2106
2092
 
2107
- if (triggerSource === 'scheduler') {
2093
+ if (triggerSource === 'schedule' || triggerSource === 'tasks') {
2108
2094
  const manager = msg();
2109
2095
  if (!manager) {
2110
2096
  throw new Error('Messaging manager not available');
@@ -2120,7 +2106,7 @@ async function executeTool(toolName, args, context, engine) {
2120
2106
  return {
2121
2107
  sent: false,
2122
2108
  skipped: true,
2123
- reason: 'A notification was already sent in this run; duplicate scheduler message was suppressed.'
2109
+ reason: 'A notification was already sent in this run; duplicate task message was suppressed.'
2124
2110
  };
2125
2111
  }
2126
2112
 
@@ -2140,7 +2126,7 @@ async function executeTool(toolName, args, context, engine) {
2140
2126
 
2141
2127
  let taskConfig = null;
2142
2128
  let taskTarget = null;
2143
- if (triggerSource === 'scheduler' && taskId) {
2129
+ if ((triggerSource === 'schedule' || triggerSource === 'tasks') && taskId) {
2144
2130
  const task = db.prepare('SELECT task_config FROM scheduled_tasks WHERE id = ? AND user_id = ?')
2145
2131
  .get(taskId, userId);
2146
2132
  if (task?.task_config) {
@@ -2169,7 +2155,7 @@ async function executeTool(toolName, args, context, engine) {
2169
2155
  addCandidate(fallbackTarget);
2170
2156
 
2171
2157
  if (candidateTargets.length === 0) {
2172
- throw new Error('No messaging target is configured for this scheduled run. Connect a platform and send at least one message on this server, or recreate the task after reconnecting.');
2158
+ throw new Error('No messaging target is configured for this task run. Connect a platform and send at least one message on this server, or recreate the task after reconnecting.');
2173
2159
  }
2174
2160
 
2175
2161
  let lastError = null;
@@ -2208,20 +2194,21 @@ async function executeTool(toolName, args, context, engine) {
2208
2194
  }
2209
2195
  }
2210
2196
 
2211
- throw (lastError || new Error('Failed to deliver scheduled notification.'));
2197
+ throw (lastError || new Error('Failed to deliver task notification.'));
2212
2198
  }
2213
2199
 
2214
2200
  engine.emit(userId, 'run:interim', { runId, message });
2215
2201
  return { sent: true, via: 'interim' };
2216
2202
  }
2217
2203
 
2218
- case 'create_scheduled_task': {
2219
- const s = sched();
2220
- if (!s) return { error: 'Scheduler not available' };
2204
+ case 'create_task': {
2205
+ const s = taskRuntime();
2206
+ if (!s) return { error: 'Task runtime not available' };
2221
2207
  try {
2222
- const task = s.createTask(userId, {
2208
+ const task = await s.createTask(userId, {
2223
2209
  name: args.name,
2224
- cronExpression: args.cron_expression,
2210
+ triggerType: args.trigger_type,
2211
+ triggerConfig: args.trigger_config,
2225
2212
  prompt: args.prompt,
2226
2213
  enabled: args.enabled !== false,
2227
2214
  model: args.model || null,
@@ -2229,46 +2216,25 @@ async function executeTool(toolName, args, context, engine) {
2229
2216
  callGreeting: args.call_greeting || null,
2230
2217
  agentId
2231
2218
  });
2232
- const callNote = args.call_to ? ` | will call ${args.call_to}` : '';
2233
- return { success: true, task, message: `Scheduled task "${args.name}" created(${args.cron_expression}${callNote})` };
2234
- } catch (err) {
2235
- return { error: err.message };
2236
- }
2237
- }
2238
-
2239
- case 'schedule_run': {
2240
- const s = sched();
2241
- if (!s) return { error: 'Scheduler not available' };
2242
- try {
2243
- const task = s.createTask(userId, {
2244
- name: args.name,
2245
- prompt: args.prompt,
2246
- runAt: args.run_at,
2247
- oneTime: true,
2248
- model: args.model || null,
2249
- callTo: args.call_to || null,
2250
- callGreeting: args.call_greeting || null,
2251
- agentId
2252
- });
2253
- return { success: true, task, message: `One-time run "${args.name}" scheduled for ${args.run_at}` };
2219
+ return { success: true, task, message: `Task "${args.name}" created.` };
2254
2220
  } catch (err) {
2255
2221
  return { error: err.message };
2256
2222
  }
2257
2223
  }
2258
2224
 
2259
- case 'list_scheduled_tasks': {
2260
- const s = sched();
2261
- if (!s) return { error: 'Scheduler not available' };
2225
+ case 'list_tasks': {
2226
+ const s = taskRuntime();
2227
+ if (!s) return { error: 'Task runtime not available' };
2262
2228
  const tasks = s.listTasks(userId).filter((task) => !agentId || task.agentId === agentId);
2263
2229
  return { tasks, count: tasks.length };
2264
2230
  }
2265
2231
 
2266
- case 'delete_scheduled_task': {
2267
- const s = sched();
2268
- if (!s) return { error: 'Scheduler not available' };
2232
+ case 'delete_task': {
2233
+ const s = taskRuntime();
2234
+ if (!s) return { error: 'Task runtime not available' };
2269
2235
  try {
2270
2236
  const task = db.prepare('SELECT agent_id FROM scheduled_tasks WHERE id = ? AND user_id = ?').get(args.task_id, userId);
2271
- if (!task || task.agent_id !== agentId) return { error: 'Scheduled task not found for this agent.' };
2237
+ if (!task || task.agent_id !== agentId) return { error: 'Task not found for this agent.' };
2272
2238
  s.deleteTask(args.task_id, userId);
2273
2239
  return { success: true, deleted: args.task_id };
2274
2240
  } catch (err) {
@@ -2276,21 +2242,22 @@ async function executeTool(toolName, args, context, engine) {
2276
2242
  }
2277
2243
  }
2278
2244
 
2279
- case 'update_scheduled_task': {
2280
- const s = sched();
2281
- if (!s) return { error: 'Scheduler not available' };
2245
+ case 'update_task': {
2246
+ const s = taskRuntime();
2247
+ if (!s) return { error: 'Task runtime not available' };
2282
2248
  try {
2283
2249
  const existing = db.prepare('SELECT agent_id FROM scheduled_tasks WHERE id = ? AND user_id = ?').get(args.task_id, userId);
2284
- if (!existing || existing.agent_id !== agentId) return { error: 'Scheduled task not found for this agent.' };
2250
+ if (!existing || existing.agent_id !== agentId) return { error: 'Task not found for this agent.' };
2285
2251
  const updates = {};
2286
2252
  if (args.name !== undefined) updates.name = args.name;
2287
- if (args.cron_expression !== undefined) updates.cronExpression = args.cron_expression;
2253
+ if (args.trigger_type !== undefined) updates.triggerType = args.trigger_type;
2254
+ if (args.trigger_config !== undefined) updates.triggerConfig = args.trigger_config;
2288
2255
  if (args.prompt !== undefined) updates.prompt = args.prompt;
2289
2256
  if (args.enabled !== undefined) updates.enabled = args.enabled;
2290
2257
  if (args.model !== undefined) updates.model = args.model || null;
2291
2258
  if (args.call_to !== undefined) updates.callTo = args.call_to || null;
2292
2259
  if (args.call_greeting !== undefined) updates.callGreeting = args.call_greeting || null;
2293
- const updated = s.updateTask(args.task_id, userId, updates);
2260
+ const updated = await s.updateTask(args.task_id, userId, updates);
2294
2261
  return { success: true, task: updated };
2295
2262
  } catch (err) {
2296
2263
  return { error: err.message };
@@ -2301,7 +2268,7 @@ async function executeTool(toolName, args, context, engine) {
2301
2268
  const widgetService = widgets();
2302
2269
  if (!widgetService) return { error: 'Widget service not available' };
2303
2270
  try {
2304
- const widget = widgetService.createWidget(userId, {
2271
+ const widget = await widgetService.createWidget(userId, {
2305
2272
  name: args.name,
2306
2273
  template: args.template,
2307
2274
  layoutVariant: args.layout_variant,
@@ -2352,7 +2319,7 @@ async function executeTool(toolName, args, context, engine) {
2352
2319
  if (!existing || existing.agentId !== agentId) {
2353
2320
  return { error: 'Widget not found for this agent.' };
2354
2321
  }
2355
- const widget = widgetService.updateWidget(userId, args.widget_id, {
2322
+ const widget = await widgetService.updateWidget(userId, args.widget_id, {
2356
2323
  name: args.name,
2357
2324
  template: args.template,
2358
2325
  layoutVariant: args.layout_variant,
@@ -214,7 +214,7 @@ class CommandRouter {
214
214
  '- `/status` - show current run/queue status\n' +
215
215
  '- `/tools` - list available built-in tools\n' +
216
216
  '- `/memory` - quick memory summary\n' +
217
- '- `/tasks` - list latest tasks and scheduled jobs\n' +
217
+ '- `/tasks` - list latest task runs and configured tasks\n' +
218
218
  '- `/models` - list currently available models\n' +
219
219
  '- `/skills` - list enabled skills\n' +
220
220
  '- `/help` - show this message\n\n' +
@@ -288,18 +288,26 @@ class CommandRouter {
288
288
 
289
289
  if (mode === 'scheduled') {
290
290
  const scheduled = db
291
- .prepare('SELECT id, name, enabled, cron_expression, run_at, one_time, last_run FROM scheduled_tasks WHERE user_id = ? AND agent_id = ? ORDER BY created_at DESC')
291
+ .prepare('SELECT id, name, enabled, trigger_type, trigger_config, last_run FROM scheduled_tasks WHERE user_id = ? AND agent_id = ? ORDER BY created_at DESC')
292
292
  .all(userId, agentId);
293
293
  if (!scheduled.length) {
294
- return { handled: true, content: '**Tasks**\n- No scheduled tasks found.' };
294
+ return { handled: true, content: '**Tasks**\n- No tasks found.' };
295
295
  }
296
296
  const lines = scheduled.map((task) => {
297
- const scheduleLabel = task.one_time ? `one-time at ${task.run_at || 'unknown'}` : (task.cron_expression || 'unspecified');
298
- return `- #${task.id} ${task.name} [${task.enabled ? 'enabled' : 'disabled'}] - ${scheduleLabel}`;
297
+ let triggerLabel = task.trigger_type || 'schedule';
298
+ try {
299
+ const config = JSON.parse(task.trigger_config || '{}') || {};
300
+ if (triggerLabel === 'schedule') {
301
+ triggerLabel = config.mode === 'one_time'
302
+ ? `one-time at ${config.runAt || 'unknown'}`
303
+ : (config.cronExpression || 'schedule');
304
+ }
305
+ } catch {}
306
+ return `- #${task.id} ${task.name} [${task.enabled ? 'enabled' : 'disabled'}] - ${triggerLabel}`;
299
307
  });
300
308
  return {
301
309
  handled: true,
302
- content: `**Scheduled Tasks (${scheduled.length})**\n${lines.join('\n')}`
310
+ content: `**Tasks (${scheduled.length})**\n${lines.join('\n')}`
303
311
  };
304
312
  }
305
313
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('crypto');
4
+ const EventEmitter = require('events');
4
5
  const fs = require('fs');
5
6
  const path = require('path');
6
7
  const db = require('../../../db/database');
@@ -205,8 +206,9 @@ function simplifyMessage(msg = {}, fallbackChatId = '') {
205
206
  };
206
207
  }
207
208
 
208
- class WhatsAppPersonalProvider {
209
+ class WhatsAppPersonalProvider extends EventEmitter {
209
210
  constructor(options = {}) {
211
+ super();
210
212
  this.key = 'whatsapp_personal';
211
213
  this.label = 'WhatsApp';
212
214
  this.description =
@@ -779,6 +781,24 @@ class WhatsAppPersonalProvider {
779
781
  ...existingChat,
780
782
  lastMessageAt: simplified.timestamp,
781
783
  });
784
+
785
+ const userId = target.userId || null;
786
+ const agentId = target.agentId || null;
787
+ const connectionId = target.connectionId || null;
788
+ if (userId && agentId && connectionId && simplified.fromMe !== true) {
789
+ this.emit('message', {
790
+ userId,
791
+ agentId,
792
+ connectionId,
793
+ chatId: simplified.chatId,
794
+ sender: simplified.sender,
795
+ senderTag: simplified.senderTag,
796
+ messageId: simplified.id,
797
+ text: simplified.text,
798
+ timestamp: simplified.timestamp,
799
+ isGroup: String(simplified.chatId || '').endsWith('@g.us'),
800
+ });
801
+ }
782
802
  }
783
803
 
784
804
  _normalizeChatId(value) {
@@ -900,6 +920,8 @@ class WhatsAppPersonalProvider {
900
920
 
901
921
  const client = existing || {
902
922
  connectionId: connection.id,
923
+ userId: connection.user_id,
924
+ agentId: connection.agent_id,
903
925
  authDir,
904
926
  socket: null,
905
927
  chats: new Map(),
@@ -10,7 +10,7 @@ const { MultiStepOrchestrator } = require('./ai/multiStep');
10
10
  const { SkillRunner } = require('./ai/toolRunner');
11
11
  const { CommandRouter } = require('./commands/router');
12
12
  const { MessagingManager } = require('./messaging/manager');
13
- const { Scheduler } = require('./scheduler/cron');
13
+ const { TaskRuntime } = require('./tasks/runtime');
14
14
  const { WidgetService } = require('./widgets/service');
15
15
  const { setupWebSocket } = require('./websocket');
16
16
  const { registerMessagingAutomation } = require('./messaging/automation');
@@ -445,12 +445,12 @@ function restoreMcpClients(mcpClient) {
445
445
  }
446
446
  }
447
447
 
448
- function startScheduler(app, io, agentEngine) {
449
- const scheduler = registerLocal(app, 'scheduler', new Scheduler(io, agentEngine, app));
450
- agentEngine.scheduler = scheduler;
451
- scheduler.start();
452
- logServiceReady('Scheduler started');
453
- return scheduler;
448
+ function startTaskRuntime(app, io, agentEngine) {
449
+ const taskRuntime = registerLocal(app, 'taskRuntime', new TaskRuntime(io, agentEngine, app));
450
+ agentEngine.taskRuntime = taskRuntime;
451
+ taskRuntime.start();
452
+ logServiceReady('Task runtime started');
453
+ return taskRuntime;
454
454
  }
455
455
 
456
456
  function configureRealtime(app, io, services) {
@@ -459,7 +459,7 @@ function configureRealtime(app, io, services) {
459
459
  messagingManager: services.messagingManager,
460
460
  mcpClient: services.mcpClient,
461
461
  integrationManager: services.integrationManager,
462
- scheduler: services.scheduler,
462
+ taskRuntime: services.taskRuntime,
463
463
  recordingManager: services.recordingManager,
464
464
  memoryManager: services.memoryManager,
465
465
  voiceRuntimeManager: services.voiceRuntimeManager,
@@ -524,14 +524,14 @@ async function startServices(app, io) {
524
524
  agentEngine,
525
525
  });
526
526
 
527
- const scheduler = startScheduler(app, io, agentEngine);
527
+ const taskRuntime = startTaskRuntime(app, io, agentEngine);
528
528
 
529
529
  configureRealtime(app, io, {
530
530
  agentEngine,
531
531
  messagingManager,
532
532
  integrationManager,
533
533
  mcpClient,
534
- scheduler,
534
+ taskRuntime,
535
535
  recordingManager,
536
536
  memoryManager,
537
537
  voiceRuntimeManager,
@@ -551,12 +551,12 @@ async function stopServices(app) {
551
551
  const tasks = [];
552
552
  console.log('[Services] Stopping services');
553
553
 
554
- if (app.locals.scheduler) {
554
+ if (app.locals.taskRuntime) {
555
555
  try {
556
- app.locals.scheduler.stop();
557
- logServiceReady('Scheduler stopped');
556
+ app.locals.taskRuntime.stop();
557
+ logServiceReady('Task runtime stopped');
558
558
  } catch (err) {
559
- console.error('[Scheduler] Stop error:', getErrorMessage(err));
559
+ console.error('[Tasks] Stop error:', getErrorMessage(err));
560
560
  }
561
561
  }
562
562
 
@@ -897,15 +897,15 @@ class MemoryManager {
897
897
 
898
898
  const queryTokens = tokenizeRecallQuery(query);
899
899
  if (queryTokens.length) {
900
- const recentSchedulerRuns = db.prepare(
900
+ const recentTaskRuns = db.prepare(
901
901
  `SELECT title, final_response, completed_at
902
902
  FROM agent_runs
903
- WHERE user_id = ? AND agent_id = ? AND trigger_source = 'scheduler' AND status = 'completed'
903
+ WHERE user_id = ? AND agent_id = ? AND trigger_source IN ('schedule', 'tasks') AND status = 'completed'
904
904
  ORDER BY completed_at DESC, created_at DESC
905
905
  LIMIT 12`
906
906
  ).all(userId, agentId);
907
907
 
908
- const schedulerMatches = recentSchedulerRuns
908
+ const taskMatches = recentTaskRuns
909
909
  .map((run) => ({
910
910
  ...run,
911
911
  score: scoreSchedulerRunMatch(queryTokens, run.title, run.final_response),
@@ -913,14 +913,14 @@ class MemoryManager {
913
913
  .filter((run) => run.score > 0)
914
914
  .slice(0, 3);
915
915
 
916
- if (schedulerMatches.length) {
917
- const schedulerLines = schedulerMatches.map((run) => {
916
+ if (taskMatches.length) {
917
+ const taskLines = taskMatches.map((run) => {
918
918
  const when = run.completed_at ? String(run.completed_at) : 'unknown time';
919
- const title = String(run.title || 'scheduler task').replace(/\s+/g, ' ').trim();
919
+ const title = String(run.title || 'task').replace(/\s+/g, ' ').trim();
920
920
  const outcome = buildExcerpt(String(run.final_response || ''), query) || String(run.final_response || '').slice(0, 180);
921
921
  return `- ${when}: ${title} -> ${outcome || '(no final response stored)'}`;
922
922
  });
923
- sections.push(`Relevant recent scheduler runs:\n${schedulerLines.join('\n')}`);
923
+ sections.push(`Relevant recent task runs:\n${taskLines.join('\n')}`);
924
924
  }
925
925
  }
926
926
 
@@ -21,7 +21,7 @@ function isTransientOperationalMemory(content) {
21
21
 
22
22
  const lower = text.toLowerCase();
23
23
  const hasRecency = /\b(recent|latest|current|today|tonight|just now|this run|last run)\b/.test(lower);
24
- const hasRunEntity = /\b(scheduler|scheduled task|cron job|agent run|task run|workflow run|job run)\b/.test(lower);
24
+ const hasRunEntity = /\b(task|scheduled run|schedule run|agent run|task run|workflow run|job run)\b/.test(lower);
25
25
  const hasExecutionState = /\b(status|completed|succeeded|failed|errored|finished|started|triggered|executed|ran)\b/.test(lower);
26
26
 
27
27
  if (hasRecency && hasRunEntity && hasExecutionState) {
@@ -18,10 +18,6 @@ const PLATFORM_FORMATTING = {
18
18
  telnyx: {
19
19
  spokenOnly: true,
20
20
  inlineCode: false,
21
- },
22
- waveshare_wearable: {
23
- spokenOnly: false,
24
- inlineCode: true,
25
21
  }
26
22
  };
27
23
 
@@ -9,7 +9,6 @@ const { WhatsAppPlatform } = require('./whatsapp');
9
9
  const { TelnyxVoicePlatform } = require('./telnyx');
10
10
  const { DiscordPlatform } = require('./discord');
11
11
  const { TelegramPlatform } = require('./telegram');
12
- const { WaveshareWearablePlatform } = require('./waveshare_wearable');
13
12
  const {
14
13
  SlackPlatform,
15
14
  GoogleChatPlatform,
@@ -89,7 +88,6 @@ class MessagingManager extends EventEmitter {
89
88
  feishu: createGenericPlatformClass('feishu'),
90
89
  line: LinePlatform,
91
90
  mattermost: MattermostPlatform,
92
- waveshare_wearable: WaveshareWearablePlatform,
93
91
  nextcloud_talk: createGenericPlatformClass('nextcloud_talk'),
94
92
  nostr: createGenericPlatformClass('nostr'),
95
93
  synology_chat: createGenericPlatformClass('synology_chat'),
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ ensureOwnedIntegrationConnection,
5
+ normalizeBoolean,
6
+ normalizeTrimmedText,
7
+ } = require('../security');
8
+
9
+ module.exports = {
10
+ type: 'gmail_message_received',
11
+ label: 'Gmail Message Received',
12
+ providerKey: 'google_workspace',
13
+ appKey: 'gmail',
14
+ async validateConfig(config = {}, context = {}) {
15
+ const connection = ensureOwnedIntegrationConnection(context.integrationManager, {
16
+ userId: context.userId,
17
+ agentId: context.agentId,
18
+ connectionId: config.connectionId || config.connection_id,
19
+ providerKey: 'google_workspace',
20
+ appKey: 'gmail',
21
+ });
22
+ return {
23
+ connectionId: connection.id,
24
+ accountEmail: connection.account_email || null,
25
+ query: normalizeTrimmedText(config.query, 500),
26
+ unreadOnly: normalizeBoolean(config.unreadOnly ?? config.unread_only, false),
27
+ };
28
+ },
29
+ summarize(config = {}) {
30
+ const parts = ['Gmail'];
31
+ if (config.accountEmail) parts.push(config.accountEmail);
32
+ if (config.query) parts.push(`query: ${config.query}`);
33
+ if (config.unreadOnly) parts.push('unread only');
34
+ return parts.join(' · ');
35
+ },
36
+ };
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ module.exports = [
4
+ require('./schedule'),
5
+ require('./gmail_message_received'),
6
+ require('./outlook_email_received'),
7
+ require('./slack_message_received'),
8
+ require('./teams_message_received'),
9
+ require('./whatsapp_personal_message_received'),
10
+ ];