neoagent 2.4.1-beta.32 → 2.4.1-beta.34

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.
@@ -61,6 +61,17 @@ function generateTitle(task) {
61
61
  return cleaned.slice(0, 90);
62
62
  }
63
63
 
64
+ function buildInitialRunMetadata(options = {}) {
65
+ const metadata = {};
66
+ if (options.taskId != null && String(options.taskId).trim()) {
67
+ metadata.taskId = options.taskId;
68
+ }
69
+ if (options.widgetId != null && String(options.widgetId).trim()) {
70
+ metadata.widgetId = options.widgetId;
71
+ }
72
+ return metadata;
73
+ }
74
+
64
75
  function planningDepthForForceMode(forceMode) {
65
76
  return forceMode === 'plan_execute' ? 'deep' : 'light';
66
77
  }
@@ -1653,8 +1664,19 @@ class AgentEngine {
1653
1664
  };
1654
1665
 
1655
1666
  const runTitle = generateTitle(userMessage);
1656
- db.prepare(`INSERT OR REPLACE INTO agent_runs(id, user_id, agent_id, title, status, trigger_type, trigger_source, model)
1657
- VALUES(?, ?, ?, ?, 'running', ?, ?, ?)`).run(runId, userId, agentId, runTitle, triggerType, triggerSource, model);
1667
+ const initialRunMetadata = buildInitialRunMetadata(options);
1668
+ db.prepare(`INSERT OR REPLACE INTO agent_runs(
1669
+ id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
1670
+ ) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(
1671
+ runId,
1672
+ userId,
1673
+ agentId,
1674
+ runTitle,
1675
+ triggerType,
1676
+ triggerSource,
1677
+ model,
1678
+ Object.keys(initialRunMetadata).length ? JSON.stringify(initialRunMetadata) : null,
1679
+ );
1658
1680
 
1659
1681
  const retryMessagingState = options.messagingRetryState || {};
1660
1682
  const carriedVisibleMessage = String(retryMessagingState.lastVisibleMessage || '').trim();
@@ -1666,6 +1688,7 @@ class AgentEngine {
1666
1688
  status: 'running',
1667
1689
  aborted: false,
1668
1690
  messagingSent: false,
1691
+ noResponse: false,
1669
1692
  explicitMessageSent: carriedExplicitMessageSent,
1670
1693
  lastSentMessage: carriedExplicitMessageSent ? carriedVisibleMessage : '',
1671
1694
  sentMessages: [],
@@ -2199,7 +2222,17 @@ class AgentEngine {
2199
2222
  break;
2200
2223
  }
2201
2224
  if (iteration < maxIterations) {
2202
- const fallbackStatus = (toolExecutions.length > 0 || failedStepCount > 0 || messagingSent) ? 'continue' : 'complete';
2225
+ const proactiveRunNeedsDecision = (
2226
+ (triggerSource === 'schedule' || triggerSource === 'tasks')
2227
+ && this.activeRuns.get(runId)?.noResponse !== true
2228
+ && options.deliveryState?.noResponse !== true
2229
+ );
2230
+ const fallbackStatus = (
2231
+ proactiveRunNeedsDecision
2232
+ || toolExecutions.length > 0
2233
+ || failedStepCount > 0
2234
+ || messagingSent
2235
+ ) ? 'continue' : 'complete';
2203
2236
  const loopState = await runWithModelFallback('loop decision', () => this.decideLoopState({
2204
2237
  provider,
2205
2238
  providerName,
@@ -2596,6 +2629,18 @@ class AgentEngine {
2596
2629
  && !messagingSent
2597
2630
  && runMeta?.widgetSnapshotSaved !== true
2598
2631
  ) {
2632
+ const explicitNoResponse = (
2633
+ runMeta?.noResponse === true
2634
+ || options.deliveryState?.noResponse === true
2635
+ );
2636
+ if (
2637
+ (triggerSource === 'schedule' || triggerSource === 'tasks')
2638
+ && !explicitNoResponse
2639
+ ) {
2640
+ throw new Error(
2641
+ 'Background run ended without producing a result or an explicit no-response decision.',
2642
+ );
2643
+ }
2599
2644
  if (iteration >= maxIterations) {
2600
2645
  throw new Error(`Iteration limit reached before explicit completion after ${maxIterations} iterations.`);
2601
2646
  }
@@ -3358,4 +3403,4 @@ class AgentEngine {
3358
3403
  }
3359
3404
  }
3360
3405
 
3361
- module.exports = { AgentEngine, getProviderForUser };
3406
+ module.exports = { AgentEngine, buildInitialRunMetadata, getProviderForUser };
@@ -274,6 +274,15 @@ function markProactiveMessageSent({ runState, deliveryState, content }) {
274
274
  }
275
275
  }
276
276
 
277
+ function markProactiveNoResponse({ runState, deliveryState }) {
278
+ if (runState) {
279
+ runState.noResponse = true;
280
+ }
281
+ if (deliveryState) {
282
+ deliveryState.noResponse = true;
283
+ }
284
+ }
285
+
277
286
  function normalizeStoredSettingString(value) {
278
287
  if (value == null) return '';
279
288
  if (typeof value !== 'string') return String(value || '').trim();
@@ -2139,6 +2148,9 @@ async function executeTool(toolName, args, context, engine) {
2139
2148
  error: proactiveValidation.error,
2140
2149
  };
2141
2150
  }
2151
+ if (proactiveValidation.reason === 'no_response') {
2152
+ markProactiveNoResponse({ runState, deliveryState });
2153
+ }
2142
2154
  return {
2143
2155
  sent: false,
2144
2156
  suppressed: proactiveValidation.suppressed === true,
@@ -275,7 +275,10 @@ async function pollIntegrationTask(runtime, task) {
275
275
  const startIndex = rows.findIndex((row) => row.fingerprint === existingFingerprint);
276
276
  const pending = startIndex >= 0 ? rows.slice(startIndex + 1) : rows.slice(-1);
277
277
  for (const row of pending) {
278
- await runtime.fireTaskFromTrigger(task.id, task.user_id, row);
278
+ const result = await runtime.fireTaskFromTrigger(task.id, task.user_id, row);
279
+ if (result?.error || (result?.skipped && result.reason !== 'duplicate_trigger')) {
280
+ break;
281
+ }
279
282
  }
280
283
  }
281
284
 
@@ -62,11 +62,7 @@ function stringifyTaskResult(result) {
62
62
  if (nested) return nested;
63
63
  }
64
64
 
65
- try {
66
- return JSON.stringify(result, null, 2);
67
- } catch {
68
- return String(result || '');
69
- }
65
+ return '';
70
66
  }
71
67
 
72
68
  class TaskRuntime {
@@ -189,6 +185,8 @@ class TaskRuntime {
189
185
  manual: true,
190
186
  triggerType: task.trigger_type || 'schedule',
191
187
  triggerSource: 'manual',
188
+ }).catch((error) => {
189
+ console.error(`[Tasks] Manual task ${taskId} error:`, error.message);
192
190
  });
193
191
  return { running: true };
194
192
  }
@@ -203,9 +201,8 @@ class TaskRuntime {
203
201
  if (String(task.last_trigger_fingerprint || '') === fingerprint) {
204
202
  return { skipped: true, reason: 'duplicate_trigger' };
205
203
  }
206
- this.taskRepository.markTaskTriggered(taskId, userId, fingerprint);
207
204
 
208
- return this._executeTask(taskId, userId, {
205
+ const result = await this._executeTask(taskId, userId, {
209
206
  manual: false,
210
207
  oneTime: false,
211
208
  scheduledAt: triggerPayload.timestamp || new Date().toISOString(),
@@ -213,39 +210,58 @@ class TaskRuntime {
213
210
  triggerSource: task.trigger_type || 'schedule',
214
211
  triggerPayload: triggerPayload.context || {},
215
212
  });
213
+ if (!result?.error && !result?.skipped) {
214
+ this.taskRepository.markTaskTriggered(taskId, userId, fingerprint);
215
+ }
216
+ return result;
216
217
  }
217
218
 
218
219
  _startOneTimePoller() {
219
220
  this.oneTimePoller = cron.schedule('* * * * *', async () => {
220
- const due = this.taskRepository.listDueOneTimeTasks();
221
+ try {
222
+ await this._runDueOneTimeTasks();
223
+ } catch (error) {
224
+ console.error('[Tasks] One-time task poll failed:', error.message);
225
+ }
226
+ });
227
+ }
221
228
 
222
- for (const task of due) {
223
- this.scheduleJobs.delete(task.id);
224
- try {
225
- await this._executeTask(task.id, task.user_id, {
226
- scheduledAt: task.run_at || new Date().toISOString(),
227
- oneTime: true,
228
- triggerType: 'schedule',
229
- triggerSource: 'schedule',
230
- });
231
- } catch (err) {
232
- console.error(`[Tasks] One-time task ${task.id} error:`, err.message);
229
+ async _runDueOneTimeTasks() {
230
+ const due = this.taskRepository.listDueOneTimeTasks();
231
+
232
+ for (const task of due) {
233
+ this.scheduleJobs.delete(task.id);
234
+ try {
235
+ const result = await this._executeTask(task.id, task.user_id, {
236
+ scheduledAt: task.run_at || new Date().toISOString(),
237
+ oneTime: true,
238
+ triggerType: 'schedule',
239
+ triggerSource: 'schedule',
240
+ });
241
+ if (result?.skipped) {
242
+ continue;
233
243
  }
234
- this.taskRepository.deleteById(task.id, task.user_id);
244
+ this.taskRepository.deleteTask(task.id, task.user_id);
235
245
  this.io.to(`user:${task.user_id}`).emit('tasks:task_deleted', { taskId: task.id });
246
+ } catch (err) {
247
+ console.error(`[Tasks] One-time task ${task.id} error:`, err.message);
236
248
  }
237
- });
249
+ }
238
250
  }
239
251
 
240
252
  _startIntegrationPoller() {
241
253
  this.integrationPoller = cron.schedule(INTEGRATION_TRIGGER_POLL_CRON, async () => {
242
- const tasks = this.taskRepository.listEnabledByTriggerTypes(POLLED_TRIGGER_TYPES);
243
- for (const task of tasks) {
244
- try {
245
- await pollIntegrationTask(this, task);
246
- } catch (error) {
247
- console.error(`[Tasks] Trigger poll failed for task ${task.id}:`, error.message);
254
+ try {
255
+ const tasks = this.taskRepository.listEnabledByTriggerTypes(POLLED_TRIGGER_TYPES);
256
+ for (const task of tasks) {
257
+ try {
258
+ await pollIntegrationTask(this, task);
259
+ } catch (error) {
260
+ console.error(`[Tasks] Trigger poll failed for task ${task.id}:`, error.message);
261
+ }
248
262
  }
263
+ } catch (error) {
264
+ console.error('[Tasks] Integration trigger poll failed:', error.message);
249
265
  }
250
266
  });
251
267
  }
@@ -258,13 +274,17 @@ class TaskRuntime {
258
274
  const cronExpression = String(triggerConfig.cronExpression || '').trim();
259
275
  if (!cronExpression) return;
260
276
  const job = cron.schedule(cronExpression, async () => {
261
- await this._executeTask(task.id, task.user_id, {
262
- scheduledAt: new Date().toISOString(),
263
- manual: false,
264
- oneTime: false,
265
- triggerType: 'schedule',
266
- triggerSource: 'schedule',
267
- });
277
+ try {
278
+ await this._executeTask(task.id, task.user_id, {
279
+ scheduledAt: new Date().toISOString(),
280
+ manual: false,
281
+ oneTime: false,
282
+ triggerType: 'schedule',
283
+ triggerSource: 'schedule',
284
+ });
285
+ } catch (error) {
286
+ console.error(`[Tasks] Scheduled task ${task.id} error:`, error.message);
287
+ }
268
288
  });
269
289
  this.scheduleJobs.set(task.id, { task: job, userId: task.user_id });
270
290
  }
@@ -324,6 +344,15 @@ class TaskRuntime {
324
344
  this.taskRepository.markTaskRun(taskId, userId);
325
345
  this.io.to(`user:${userId}`).emit('tasks:task_running', { taskId, timestamp: new Date().toISOString() });
326
346
 
347
+ let normalizedConfig = taskConfig;
348
+ const taskName = task.name || `Task ${taskId}`;
349
+ const deliveryState = {
350
+ messagingSent: false,
351
+ noResponse: false,
352
+ lastSentMessage: '',
353
+ sentMessages: [],
354
+ };
355
+ let completedRunId = null;
327
356
  try {
328
357
  if (task.task_type === 'widget_refresh') {
329
358
  const widgetService = this.app?.locals?.widgetService;
@@ -339,8 +368,7 @@ class TaskRuntime {
339
368
  return result;
340
369
  }
341
370
 
342
- const normalizedConfig = this._ensureDefaultNotifyTarget(userId, agentId, taskConfig, taskId);
343
- const taskName = task.name || `Task ${taskId}`;
371
+ normalizedConfig = this._ensureDefaultNotifyTarget(userId, agentId, taskConfig, taskId);
344
372
  const triggerSummary = this._summarizeTrigger(task.trigger_type, triggerConfig);
345
373
  let notifyHint = '';
346
374
 
@@ -366,7 +394,6 @@ class TaskRuntime {
366
394
  ].filter(Boolean).join('\n\n');
367
395
 
368
396
  const conversationId = this._getTaskConversation(userId, taskId, taskName, agentId);
369
- const deliveryState = { messagingSent: false, lastSentMessage: '', sentMessages: [] };
370
397
  let attempt = 0;
371
398
  let recoveryNote = '';
372
399
  while (attempt <= MAX_AUTONOMOUS_RETRIES) {
@@ -394,6 +421,7 @@ class TaskRuntime {
394
421
  const result = typeof this.agentEngine.runWithModel === 'function'
395
422
  ? await this.agentEngine.runWithModel(userId, finalPrompt, runOptions, normalizedConfig.model || null)
396
423
  : await this.agentEngine.run(userId, finalPrompt, runOptions);
424
+ completedRunId = result?.runId || null;
397
425
  const fallbackDelivery = await this._deliverTaskResultIfNeeded({
398
426
  userId,
399
427
  agentId,
@@ -405,11 +433,30 @@ class TaskRuntime {
405
433
  if (fallbackDelivery && result && typeof result === 'object') {
406
434
  result.taskDelivery = fallbackDelivery;
407
435
  }
436
+ if (fallbackDelivery?.error) {
437
+ const deliveryError = new Error(fallbackDelivery.error);
438
+ deliveryError.code = 'TASK_DELIVERY_FAILED';
439
+ throw deliveryError;
440
+ }
441
+ if (
442
+ !deliveryState.messagingSent
443
+ && !deliveryState.noResponse
444
+ && !stringifyTaskResult(result).trim()
445
+ ) {
446
+ throw new Error(
447
+ 'Background task completed without producing a result or an explicit no-response decision.',
448
+ );
449
+ }
408
450
  this.io.to(`user:${userId}`).emit('tasks:task_complete', { taskId, result });
409
451
  return result;
410
452
  } catch (err) {
453
+ if (completedRunId) {
454
+ this.taskRepository.markAgentRunFailed(completedRunId, userId, err.message);
455
+ }
456
+ if (err?.code === 'TASK_DELIVERY_FAILED') throw err;
411
457
  if (attempt >= MAX_AUTONOMOUS_RETRIES) throw err;
412
458
  attempt += 1;
459
+ completedRunId = null;
413
460
  recoveryNote = [
414
461
  '\n\n[SYSTEM: Previous task attempt failed]',
415
462
  `Error: ${String(err?.message || 'Unknown runtime error')}`,
@@ -424,12 +471,24 @@ class TaskRuntime {
424
471
  }
425
472
  } catch (err) {
426
473
  console.error(`[Tasks] Task ${taskId} error:`, err.message);
474
+ if (err?.code !== 'TASK_DELIVERY_FAILED') {
475
+ await this._deliverTaskResultIfNeeded({
476
+ userId,
477
+ agentId,
478
+ taskId,
479
+ taskConfig: normalizedConfig,
480
+ result: {
481
+ content: `Background task "${taskName}" could not complete after retrying. Check the task run logs for details.`,
482
+ },
483
+ deliveryState,
484
+ });
485
+ }
427
486
  this.io.to(`user:${userId}`).emit('tasks:task_skipped', {
428
487
  taskId,
429
488
  reason: 'execution_failed',
430
489
  timestamp: new Date().toISOString(),
431
490
  });
432
- return { skipped: false, error: err.message };
491
+ return { skipped: false, error: err.message, runId: completedRunId };
433
492
  }
434
493
  }
435
494
 
@@ -518,7 +577,10 @@ class TaskRuntime {
518
577
  triggerSummary,
519
578
  nextRun: triggerType === 'schedule' ? scheduleAdapter.nextRun(triggerConfig) : null,
520
579
  enabled: !!row.enabled,
521
- lastRun: row.last_run || null,
580
+ lastRun: row.last_run_started_at || row.last_run || null,
581
+ lastRunId: row.last_run_id || null,
582
+ lastRunStatus: row.last_run_status || null,
583
+ lastRunError: row.last_run_error || null,
522
584
  lastTriggeredAt: row.last_triggered_at || null,
523
585
  taskType: row.task_type || 'agent_prompt',
524
586
  taskConfig,
@@ -618,13 +680,18 @@ class TaskRuntime {
618
680
  result,
619
681
  deliveryState,
620
682
  }) {
621
- if (deliveryState?.messagingSent || taskConfig.callTo) return null;
622
- const manager = this.app?.locals?.messagingManager || this.agentEngine?.messagingManager || null;
623
- if (!manager) return null;
624
-
683
+ if (deliveryState?.messagingSent || deliveryState?.noResponse || taskConfig.callTo) return null;
625
684
  const targets = this._buildNotifyTargets(userId, agentId, taskConfig);
626
685
  if (!targets.length) return null;
627
686
 
687
+ const manager = this.app?.locals?.messagingManager || this.agentEngine?.messagingManager || null;
688
+ if (!manager) {
689
+ return {
690
+ sent: false,
691
+ error: 'Messaging delivery is unavailable on this server.',
692
+ };
693
+ }
694
+
628
695
  let lastError = null;
629
696
  for (const target of targets) {
630
697
  const message = normalizeOutgoingMessageForPlatform(
@@ -676,6 +743,10 @@ class TaskRuntime {
676
743
 
677
744
  if (lastError) {
678
745
  console.error(`[Tasks] Task ${taskId} notification delivery failed:`, lastError.message);
746
+ return {
747
+ sent: false,
748
+ error: lastError.message,
749
+ };
679
750
  }
680
751
  return null;
681
752
  }
@@ -59,16 +59,34 @@ class TaskRepository {
59
59
  }
60
60
 
61
61
  listTasksForAgent(userId, agentId, includeLegacyMainTasks) {
62
+ const select = `
63
+ SELECT
64
+ scheduled_tasks.*,
65
+ latest_run.id AS last_run_id,
66
+ latest_run.status AS last_run_status,
67
+ latest_run.error AS last_run_error,
68
+ latest_run.created_at AS last_run_started_at
69
+ FROM scheduled_tasks
70
+ LEFT JOIN agent_runs AS latest_run ON latest_run.id = (
71
+ SELECT candidate.id
72
+ FROM agent_runs AS candidate
73
+ WHERE candidate.user_id = scheduled_tasks.user_id
74
+ AND json_valid(candidate.metadata_json)
75
+ AND CAST(json_extract(candidate.metadata_json, '$.taskId') AS TEXT) = CAST(scheduled_tasks.id AS TEXT)
76
+ ORDER BY candidate.created_at DESC, candidate.rowid DESC
77
+ LIMIT 1
78
+ )`;
62
79
  return includeLegacyMainTasks
63
80
  ? db.prepare(
64
- `SELECT * FROM scheduled_tasks
65
- WHERE user_id = ? AND (agent_id = ? OR agent_id IS NULL)
66
- ORDER BY created_at DESC`
81
+ `${select}
82
+ WHERE scheduled_tasks.user_id = ?
83
+ AND (scheduled_tasks.agent_id = ? OR scheduled_tasks.agent_id IS NULL)
84
+ ORDER BY scheduled_tasks.created_at DESC`
67
85
  ).all(userId, agentId)
68
86
  : db.prepare(
69
- `SELECT * FROM scheduled_tasks
70
- WHERE user_id = ? AND agent_id = ?
71
- ORDER BY created_at DESC`
87
+ `${select}
88
+ WHERE scheduled_tasks.user_id = ? AND scheduled_tasks.agent_id = ?
89
+ ORDER BY scheduled_tasks.created_at DESC`
72
90
  ).all(userId, agentId);
73
91
  }
74
92
 
@@ -126,6 +144,17 @@ class TaskRepository {
126
144
  db.prepare('UPDATE scheduled_tasks SET last_run = datetime(\'now\') WHERE id = ? AND user_id = ?').run(taskId, userId);
127
145
  }
128
146
 
147
+ markAgentRunFailed(runId, userId, error) {
148
+ db.prepare(
149
+ `UPDATE agent_runs
150
+ SET status = 'failed',
151
+ error = ?,
152
+ updated_at = datetime('now'),
153
+ completed_at = COALESCE(completed_at, datetime('now'))
154
+ WHERE id = ? AND user_id = ?`
155
+ ).run(String(error || 'Task execution failed.'), runId, userId);
156
+ }
157
+
129
158
  updateTaskConfig(taskId, userId, taskConfig) {
130
159
  db.prepare('UPDATE scheduled_tasks SET task_config = ? WHERE id = ? AND user_id = ?')
131
160
  .run(JSON.stringify(taskConfig), taskId, userId);