neoagent 2.0.6 → 2.0.8

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.
@@ -110,4 +110,24 @@ router.post('/:sessionId/retry', async (req, res) => {
110
110
  }
111
111
  });
112
112
 
113
+ router.delete('/:sessionId/segments/:segmentId', (req, res) => {
114
+ try {
115
+ const manager = req.app.locals.recordingManager;
116
+ const session = manager.deleteTranscriptSegment(
117
+ req.session.userId,
118
+ req.params.sessionId,
119
+ req.params.segmentId,
120
+ );
121
+ res.json({ session });
122
+ } catch (err) {
123
+ const message = sanitizeError(err);
124
+ const status = /not found/i.test(message)
125
+ ? 404
126
+ : /positive integer/i.test(message)
127
+ ? 400
128
+ : 500;
129
+ res.status(status).json({ error: message });
130
+ }
131
+ });
132
+
113
133
  module.exports = router;
@@ -14,13 +14,13 @@ router.get('/', (req, res) => {
14
14
  // Create a new scheduled task
15
15
  router.post('/', (req, res) => {
16
16
  try {
17
- const { name, cronExpression, prompt, enabled } = req.body;
17
+ const { name, cronExpression, prompt, enabled, model } = req.body;
18
18
  if (!name || !cronExpression || !prompt) {
19
19
  return res.status(400).json({ error: 'name, cronExpression, and prompt required' });
20
20
  }
21
21
 
22
22
  const scheduler = req.app.locals.scheduler;
23
- const task = scheduler.createTask(req.session.userId, { name, cronExpression, prompt, enabled });
23
+ const task = scheduler.createTask(req.session.userId, { name, cronExpression, prompt, enabled, model });
24
24
  res.status(201).json(task);
25
25
  } catch (err) {
26
26
  res.status(400).json({ error: sanitizeError(err) });
@@ -1,5 +1,6 @@
1
1
  const express = require('express');
2
2
  const fs = require('fs');
3
+ const path = require('path');
3
4
  const router = express.Router();
4
5
  const db = require('../db/database');
5
6
  const { requireAuth } = require('../middleware/auth');
@@ -28,6 +29,17 @@ function readUpdateStatus() {
28
29
  }
29
30
  }
30
31
 
32
+ function writeUpdateStatus(patch) {
33
+ const next = {
34
+ ...readUpdateStatus(),
35
+ ...patch,
36
+ updatedAt: new Date().toISOString()
37
+ };
38
+ fs.mkdirSync(path.dirname(UPDATE_STATUS_FILE), { recursive: true });
39
+ fs.writeFileSync(UPDATE_STATUS_FILE, JSON.stringify(next, null, 2));
40
+ return next;
41
+ }
42
+
31
43
  // Get supported models metadata
32
44
  router.get('/meta/models', async (req, res) => {
33
45
  const { getSupportedModels } = require('../services/ai/models');
@@ -221,6 +233,16 @@ router.post('/update', (req, res) => {
221
233
  return res.status(409).json({ success: false, error: 'An update is already running' });
222
234
  }
223
235
  console.log('[Settings] Triggering update-runner...');
236
+ writeUpdateStatus({
237
+ state: 'running',
238
+ progress: 1,
239
+ phase: 'starting',
240
+ message: 'Launching update job',
241
+ startedAt: new Date().toISOString(),
242
+ completedAt: null,
243
+ changelog: [],
244
+ logs: []
245
+ });
224
246
 
225
247
  // Spawn detached runner so status survives server restarts.
226
248
  const child = spawn(process.execPath, ['scripts/update-runner.js'], {
@@ -229,6 +251,16 @@ router.post('/update', (req, res) => {
229
251
  cwd: APP_DIR
230
252
  });
231
253
 
254
+ child.once('error', (error) => {
255
+ writeUpdateStatus({
256
+ state: 'failed',
257
+ progress: 100,
258
+ phase: 'failed',
259
+ message: `Failed to launch update job: ${error.message}`,
260
+ completedAt: new Date().toISOString()
261
+ });
262
+ });
263
+
232
264
  child.unref();
233
265
  res.json({ success: true, message: 'Update triggered', pid: child.pid });
234
266
  });
@@ -437,6 +437,7 @@ function getAvailableTools(app, options = {}) {
437
437
  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.' },
438
438
  prompt: { type: 'string', description: 'The prompt/instructions the agent will run when triggered. Be specific about what to do and who to notify.' },
439
439
  enabled: { type: 'boolean', description: 'Whether to activate immediately (default true)' },
440
+ model: { type: 'string', description: 'Optional specific AI model ID to force for this task. Omit to use the normal automatic/default model selection.' },
440
441
  call_to: { type: 'string', description: 'E.164 phone number to call via Telnyx when this task fires, e.g. "+12125550100".' },
441
442
  call_greeting: { type: 'string', description: 'Opening sentence spoken to the user when the call is answered. Required if call_to is set.' }
442
443
  },
@@ -452,6 +453,7 @@ function getAvailableTools(app, options = {}) {
452
453
  name: { type: 'string', description: 'Short descriptive name, e.g. "Remind about meeting"' },
453
454
  run_at: { type: 'string', description: 'ISO 8601 datetime when the run should fire, e.g. "2026-03-09T22:00:00"' },
454
455
  prompt: { type: 'string', description: 'The prompt/instructions the agent will execute at that time. Be specific.' },
456
+ model: { type: 'string', description: 'Optional specific AI model ID to force for this run. Omit to use the normal automatic/default model selection.' },
455
457
  call_to: { type: 'string', description: 'Optional E.164 phone number to call via Telnyx when this fires.' },
456
458
  call_greeting: { type: 'string', description: 'Opening sentence spoken when the Telnyx call is answered.' }
457
459
  },
@@ -485,6 +487,7 @@ function getAvailableTools(app, options = {}) {
485
487
  cron_expression: { type: 'string', description: 'New cron expression, e.g. "0 8 * * *" for daily at 8am' },
486
488
  prompt: { type: 'string', description: 'New prompt/instructions for the task' },
487
489
  enabled: { type: 'boolean', description: 'Enable or disable the task' },
490
+ 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.' },
488
491
  call_to: { type: 'string', description: 'E.164 phone number to call via Telnyx when this task fires. Set to empty string to remove.' },
489
492
  call_greeting: { type: 'string', description: 'New opening sentence spoken when the Telnyx call is answered.' }
490
493
  },
@@ -997,6 +1000,7 @@ async function executeTool(toolName, args, context, engine) {
997
1000
  cronExpression: args.cron_expression,
998
1001
  prompt: args.prompt,
999
1002
  enabled: args.enabled !== false,
1003
+ model: args.model || null,
1000
1004
  callTo: args.call_to || null,
1001
1005
  callGreeting: args.call_greeting || null
1002
1006
  });
@@ -1016,6 +1020,7 @@ async function executeTool(toolName, args, context, engine) {
1016
1020
  prompt: args.prompt,
1017
1021
  runAt: args.run_at,
1018
1022
  oneTime: true,
1023
+ model: args.model || null,
1019
1024
  callTo: args.call_to || null,
1020
1025
  callGreeting: args.call_greeting || null
1021
1026
  });
@@ -1052,6 +1057,7 @@ async function executeTool(toolName, args, context, engine) {
1052
1057
  if (args.cron_expression !== undefined) updates.cronExpression = args.cron_expression;
1053
1058
  if (args.prompt !== undefined) updates.prompt = args.prompt;
1054
1059
  if (args.enabled !== undefined) updates.enabled = args.enabled;
1060
+ if (args.model !== undefined) updates.model = args.model || null;
1055
1061
  if (args.call_to !== undefined) updates.callTo = args.call_to || null;
1056
1062
  if (args.call_greeting !== undefined) updates.callGreeting = args.call_greeting || null;
1057
1063
  const updated = s.updateTask(args.task_id, userId, updates);
@@ -441,9 +441,7 @@ class RecordingManager {
441
441
  }
442
442
  })();
443
443
 
444
- const transcriptText = ordered
445
- .map((segment) => `[${this.#formatTimestamp(segment.startMs)}] ${segment.text}`)
446
- .join('\n');
444
+ const transcriptText = this.#composeTranscriptText(ordered);
447
445
  db.prepare(`
448
446
  UPDATE recording_sessions
449
447
  SET
@@ -490,6 +488,57 @@ class RecordingManager {
490
488
  return this.getSession(userId, sessionId);
491
489
  }
492
490
 
491
+ deleteTranscriptSegment(userId, sessionId, segmentId) {
492
+ const session = db.prepare(`
493
+ SELECT id
494
+ FROM recording_sessions
495
+ WHERE id = ? AND user_id = ?
496
+ `).get(sessionId, userId);
497
+ if (!session) {
498
+ throw new Error('Recording session not found.');
499
+ }
500
+
501
+ const normalizedSegmentId = Number(segmentId);
502
+ if (!Number.isInteger(normalizedSegmentId) || normalizedSegmentId <= 0) {
503
+ throw new Error('segmentId must be a positive integer.');
504
+ }
505
+
506
+ const segment = db.prepare(`
507
+ SELECT id
508
+ FROM recording_transcript_segments
509
+ WHERE session_id = ? AND id = ?
510
+ `).get(sessionId, normalizedSegmentId);
511
+ if (!segment) {
512
+ throw new Error('Transcript segment not found.');
513
+ }
514
+
515
+ const now = new Date().toISOString();
516
+ let transcriptText = '';
517
+ db.transaction(() => {
518
+ db.prepare(`
519
+ DELETE FROM recording_transcript_segments
520
+ WHERE session_id = ? AND id = ?
521
+ `).run(sessionId, normalizedSegmentId);
522
+
523
+ const remainingSegments = db.prepare(`
524
+ SELECT start_ms, text
525
+ FROM recording_transcript_segments
526
+ WHERE session_id = ?
527
+ ORDER BY start_ms ASC, id ASC
528
+ `).all(sessionId);
529
+ transcriptText = this.#composeTranscriptText(remainingSegments);
530
+
531
+ db.prepare(`
532
+ UPDATE recording_sessions
533
+ SET transcript_text = ?, updated_at = ?
534
+ WHERE id = ?
535
+ `).run(transcriptText, now, sessionId);
536
+ })();
537
+
538
+ this.#emitUpdate(userId, sessionId);
539
+ return this.getSession(userId, sessionId);
540
+ }
541
+
493
542
  async #transcribeSourceChunks(source, chunks) {
494
543
  const segments = [];
495
544
 
@@ -677,6 +726,20 @@ class RecordingManager {
677
726
  };
678
727
  }
679
728
 
729
+ #composeTranscriptText(segments) {
730
+ return (Array.isArray(segments) ? segments : [])
731
+ .map((segment) => {
732
+ const startMs = Number(segment.startMs ?? segment.start_ms) || 0;
733
+ const text = `${segment.text || ''}`.trim();
734
+ if (!text) {
735
+ return null;
736
+ }
737
+ return `[${this.#formatTimestamp(startMs)}] ${text}`;
738
+ })
739
+ .filter((line) => line != null)
740
+ .join('\n');
741
+ }
742
+
680
743
  #emitUpdate(userId, sessionId) {
681
744
  this.io?.to?.(`user:${userId}`)?.emit('recordings:updated', { sessionId });
682
745
  }
@@ -108,7 +108,7 @@ class Scheduler {
108
108
  }
109
109
  }
110
110
 
111
- createTask(userId, { name, cronExpression, prompt, enabled = true, callTo = null, callGreeting = null, runAt = null, oneTime = false }) {
111
+ createTask(userId, { name, cronExpression, prompt, enabled = true, callTo = null, callGreeting = null, model = null, runAt = null, oneTime = false }) {
112
112
  if (oneTime) {
113
113
  if (!runAt) throw new Error('runAt is required for one-time tasks');
114
114
  const runAtDate = new Date(runAt);
@@ -116,12 +116,13 @@ class Scheduler {
116
116
 
117
117
  const config = { prompt };
118
118
  if (callTo) { config.callTo = callTo; config.callGreeting = callGreeting || ''; }
119
+ if (typeof model === 'string' && model.trim()) config.model = model.trim();
119
120
 
120
121
  const result = db.prepare(
121
122
  'INSERT INTO scheduled_tasks (user_id, name, cron_expression, run_at, one_time, task_type, task_config, enabled) VALUES (?, ?, NULL, ?, 1, ?, ?, ?)'
122
123
  ).run(userId, name, runAtDate.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ''), 'agent_prompt', JSON.stringify(config), enabled ? 1 : 0);
123
124
 
124
- return { id: result.lastInsertRowid, name, runAt: runAtDate.toISOString(), oneTime: true, enabled };
125
+ return { id: result.lastInsertRowid, name, runAt: runAtDate.toISOString(), oneTime: true, enabled, model: config.model || null };
125
126
  }
126
127
 
127
128
  if (!cronExpression || !cron.validate(cronExpression)) {
@@ -130,6 +131,7 @@ class Scheduler {
130
131
 
131
132
  const config = { prompt };
132
133
  if (callTo) { config.callTo = callTo; config.callGreeting = callGreeting || ''; }
134
+ if (typeof model === 'string' && model.trim()) config.model = model.trim();
133
135
 
134
136
  const result = db.prepare(
135
137
  'INSERT INTO scheduled_tasks (user_id, name, cron_expression, task_type, task_config, enabled) VALUES (?, ?, ?, ?, ?, ?)'
@@ -141,7 +143,7 @@ class Scheduler {
141
143
  this._scheduleTask(taskId, userId, cronExpression, config);
142
144
  }
143
145
 
144
- return { id: taskId, name, cronExpression, enabled, callTo: config.callTo || null };
146
+ return { id: taskId, name, cronExpression, enabled, callTo: config.callTo || null, model: config.model || null };
145
147
  }
146
148
 
147
149
  updateTask(taskId, userId, updates) {
@@ -157,6 +159,13 @@ class Scheduler {
157
159
  if (updates.prompt !== undefined) config.prompt = updates.prompt;
158
160
  if (updates.callTo !== undefined) config.callTo = updates.callTo || null;
159
161
  if (updates.callGreeting !== undefined) config.callGreeting = updates.callGreeting || null;
162
+ if (updates.model !== undefined) {
163
+ if (typeof updates.model === 'string' && updates.model.trim()) {
164
+ config.model = updates.model.trim();
165
+ } else {
166
+ delete config.model;
167
+ }
168
+ }
160
169
  // Clean up nulls
161
170
  if (!config.callTo) { delete config.callTo; delete config.callGreeting; }
162
171
 
@@ -178,7 +187,7 @@ class Scheduler {
178
187
  this._scheduleTask(taskId, userId, cronExpr, config);
179
188
  }
180
189
 
181
- return { id: taskId, name, cronExpression: cronExpr, enabled, callTo: config.callTo || null };
190
+ return { id: taskId, name, cronExpression: cronExpr, enabled, callTo: config.callTo || null, model: config.model || null };
182
191
  }
183
192
 
184
193
  deleteTask(taskId, userId) {
@@ -197,17 +206,21 @@ class Scheduler {
197
206
 
198
207
  listTasks(userId) {
199
208
  const tasks = db.prepare('SELECT * FROM scheduled_tasks WHERE user_id = ? ORDER BY created_at DESC').all(userId);
200
- return tasks.map(t => ({
201
- id: t.id,
202
- name: t.name,
203
- cronExpression: t.cron_expression,
204
- runAt: t.run_at || null,
205
- oneTime: !!t.one_time,
206
- enabled: !!t.enabled,
207
- lastRun: t.last_run,
208
- nextRun: t.one_time ? t.run_at : this._getNextRun(t.cron_expression),
209
- config: JSON.parse(t.task_config || '{}')
210
- }));
209
+ return tasks.map(t => {
210
+ const config = JSON.parse(t.task_config || '{}');
211
+ return {
212
+ id: t.id,
213
+ name: t.name,
214
+ cronExpression: t.cron_expression,
215
+ runAt: t.run_at || null,
216
+ oneTime: !!t.one_time,
217
+ enabled: !!t.enabled,
218
+ lastRun: t.last_run,
219
+ nextRun: t.one_time ? t.run_at : this._getNextRun(t.cron_expression),
220
+ config,
221
+ model: config.model || null
222
+ };
223
+ });
211
224
  }
212
225
 
213
226
  runTaskNow(taskId, userId) {
@@ -248,13 +261,16 @@ class Scheduler {
248
261
 
249
262
  const convId = this._getMessagingConversation(userId);
250
263
 
251
- const result = await this.agentEngine.run(userId, config.prompt + notifyHint, {
264
+ const runOptions = {
252
265
  triggerType: 'scheduler',
253
266
  triggerSource: 'scheduler',
254
267
  app: this.app,
255
268
  ...(convId ? { conversationId: convId } : {}),
256
269
  taskId,
257
- });
270
+ };
271
+ const result = typeof this.agentEngine.runWithModel === 'function'
272
+ ? await this.agentEngine.runWithModel(userId, config.prompt + notifyHint, runOptions, config.model || null)
273
+ : await this.agentEngine.run(userId, config.prompt + notifyHint, runOptions);
258
274
  this.io.to(`user:${userId}`).emit('scheduler:task_complete', { taskId, result });
259
275
  }
260
276
  } catch (err) {