neoagent 2.4.1-beta.34 → 2.4.1-beta.36

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 (30) hide show
  1. package/.env.example +10 -3
  2. package/flutter_app/lib/main_admin.dart +13 -0
  3. package/flutter_app/lib/main_controller.dart +24 -6
  4. package/flutter_app/lib/main_models.dart +14 -0
  5. package/package.json +1 -1
  6. package/server/public/.last_build_id +1 -1
  7. package/server/public/flutter_bootstrap.js +1 -1
  8. package/server/public/main.dart.js +13767 -13696
  9. package/server/routes/mcp.js +29 -13
  10. package/server/routes/memory.js +1 -0
  11. package/server/services/ai/engine.js +35 -6
  12. package/server/services/ai/systemPrompt.js +28 -22
  13. package/server/services/ai/taskAnalysis.js +4 -1
  14. package/server/services/ai/toolSelector.js +8 -1
  15. package/server/services/ai/tools.js +12 -3
  16. package/server/services/desktop/screenRecorder.js +208 -98
  17. package/server/services/desktop/screen_recorder_support.js +46 -0
  18. package/server/services/manager.js +51 -53
  19. package/server/services/mcp/client.js +208 -268
  20. package/server/services/mcp/client_support.js +172 -0
  21. package/server/services/mcp/recovery.js +116 -0
  22. package/server/services/mcp/tool_operations.js +123 -0
  23. package/server/services/memory/ingestion.js +185 -370
  24. package/server/services/memory/ingestion_coverage.js +129 -0
  25. package/server/services/memory/ingestion_documents.js +123 -0
  26. package/server/services/memory/ingestion_support.js +171 -0
  27. package/server/services/messaging/automation.js +71 -134
  28. package/server/services/messaging/inbound_queue.js +111 -0
  29. package/server/services/messaging/typing_keepalive.js +110 -0
  30. package/server/services/tasks/runtime.js +133 -21
@@ -1,175 +1,280 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
3
4
  const fs = require('fs/promises');
4
5
  const path = require('path');
5
6
  const os = require('os');
6
- const { exec } = require('child_process');
7
+ const { execFile } = require('child_process');
7
8
  const { promisify } = require('util');
8
9
  const tesseract = require('tesseract.js');
9
10
  const db = require('../../db/database');
10
11
  const { getErrorMessage } = require('../bootstrap_helpers');
12
+ const {
13
+ CLEANUP_INTERVAL_MS,
14
+ DEFAULT_CAPTURE_INTERVAL_MS,
15
+ DEFAULT_RETENTION_DAYS,
16
+ FRONTMOST_APP_SCRIPT,
17
+ MINIMUM_TEXT_LENGTH,
18
+ MIN_CAPTURE_INTERVAL_MS,
19
+ hasOpenConnectionForUser,
20
+ isExplicitlyEnabled,
21
+ parsePositiveInteger,
22
+ } = require('./screen_recorder_support');
11
23
 
12
- const execAsync = promisify(exec);
24
+ const execFileAsync = promisify(execFile);
13
25
 
14
26
  class ScreenRecorder {
15
27
  constructor(options = {}) {
16
- this.intervalMs = 10000; // 10 seconds
28
+ this.env = options.env || process.env;
29
+ this.platform = options.platform || process.platform;
30
+ this.db = options.db || db;
31
+ this.fs = options.fs || fs;
32
+ this.execFile = options.execFile || execFileAsync;
33
+ this.recognize = options.recognize || tesseract.recognize;
34
+ this.setInterval = options.setInterval || setInterval;
35
+ this.clearInterval = options.clearInterval || clearInterval;
36
+ this.now = options.now || Date.now;
37
+ this.hasActiveCaptureSessionForUser =
38
+ typeof options.hasActiveCaptureSessionForUser === 'function'
39
+ ? options.hasActiveCaptureSessionForUser
40
+ : () => false;
41
+
17
42
  this.intervalId = null;
18
43
  this.cleanupIntervalId = null;
19
44
  this.isRecording = false;
20
- this.isProcessing = false;
21
- this.tempFilePath = path.join(os.tmpdir(), `neoagent-screen-${Date.now()}.png`);
45
+ this.activeCapturePromise = null;
46
+ this.tempFilePath = options.tempFilePath || path.join(
47
+ os.tmpdir(),
48
+ `neoagent-screen-${process.pid}-${crypto.randomUUID()}.png`,
49
+ );
22
50
  this.lastBenignSkipAt = 0;
23
- this.hasActiveRemoteCaptureSession = typeof options.hasActiveRemoteCaptureSession === 'function'
24
- ? options.hasActiveRemoteCaptureSession
25
- : () => false;
51
+ this.lastErrorLogAt = 0;
52
+ this.ownerUserId = null;
53
+ this.intervalMs = DEFAULT_CAPTURE_INTERVAL_MS;
54
+ this.retentionDays = DEFAULT_RETENTION_DAYS;
55
+ this.state = 'idle';
56
+ this.reason = 'not started';
57
+ this.lastCaptureAt = null;
58
+ this.lastSuccessAt = null;
59
+ this.lastSkipReason = null;
60
+ this.lastError = null;
26
61
  }
27
62
 
28
- _isCaptureInactiveApp(appName) {
29
- const normalized = String(appName || '').trim().toLowerCase();
30
- return normalized === '' || normalized === 'loginwindow' || normalized === 'screensaverengine';
63
+ getStatus() {
64
+ return {
65
+ state: this.state,
66
+ reason: this.reason,
67
+ ownerUserId: this.ownerUserId,
68
+ intervalMs: this.intervalMs,
69
+ retentionDays: this.retentionDays,
70
+ lastCaptureAt: this.lastCaptureAt,
71
+ lastSuccessAt: this.lastSuccessAt,
72
+ lastSkipReason: this.lastSkipReason,
73
+ lastError: this.lastError,
74
+ };
31
75
  }
32
76
 
33
- _isBenignCaptureError(message) {
34
- const text = String(message || '').toLowerCase();
35
- return (
36
- text.includes('operation not permitted') ||
37
- text.includes('not authorized') ||
38
- text.includes('user canceled') ||
39
- text.includes('cgwindowlistcreateimage') ||
40
- text.includes('screencapture') ||
41
- text.includes('timed out')
42
- );
77
+ _setInactiveState(state, reason) {
78
+ this.state = state;
79
+ this.reason = reason;
80
+ this.isRecording = false;
81
+ return this.getStatus();
43
82
  }
44
83
 
45
- _logBenignSkip(reason) {
46
- const now = Date.now();
47
- if (now - this.lastBenignSkipAt < 5 * 60 * 1000) {
48
- return;
84
+ _loadConfiguration() {
85
+ const ownerUserId = parsePositiveInteger(
86
+ this.env.NEOAGENT_SCREEN_RECORDER_USER_ID,
87
+ 'NEOAGENT_SCREEN_RECORDER_USER_ID',
88
+ null,
89
+ );
90
+ if (ownerUserId == null) {
91
+ throw new Error('NEOAGENT_SCREEN_RECORDER_USER_ID is required when screen recording is enabled.');
49
92
  }
50
- this.lastBenignSkipAt = now;
51
- console.log(`[ScreenRecorder] Capture skipped: ${reason}`);
93
+
94
+ this.intervalMs = parsePositiveInteger(
95
+ this.env.NEOAGENT_SCREEN_RECORDER_INTERVAL_MS,
96
+ 'NEOAGENT_SCREEN_RECORDER_INTERVAL_MS',
97
+ DEFAULT_CAPTURE_INTERVAL_MS,
98
+ MIN_CAPTURE_INTERVAL_MS,
99
+ );
100
+ this.retentionDays = parsePositiveInteger(
101
+ this.env.NEOAGENT_SCREEN_RECORDER_RETENTION_DAYS,
102
+ 'NEOAGENT_SCREEN_RECORDER_RETENTION_DAYS',
103
+ DEFAULT_RETENTION_DAYS,
104
+ );
105
+ this.ownerUserId = ownerUserId;
106
+ }
107
+
108
+ _ownerExists() {
109
+ return Boolean(
110
+ this.db.prepare('SELECT id FROM users WHERE id = ?').get(this.ownerUserId),
111
+ );
52
112
  }
53
113
 
54
114
  start() {
55
- const enabledEnv = String(process.env.NEOAGENT_SCREEN_RECORDER_ENABLED || '').trim().toLowerCase();
56
- if (enabledEnv === '0' || enabledEnv === 'false' || enabledEnv === 'off' || enabledEnv === 'no') {
57
- console.log('[ScreenRecorder] Not starting: disabled by NEOAGENT_SCREEN_RECORDER_ENABLED.');
58
- return;
115
+ if (this.isRecording) {
116
+ return this.getStatus();
59
117
  }
60
118
 
61
- if (process.platform !== 'darwin') {
62
- console.log('[ScreenRecorder] Not starting: Screen recording is currently macOS only.');
63
- return;
119
+ if (!isExplicitlyEnabled(this.env.NEOAGENT_SCREEN_RECORDER_ENABLED)) {
120
+ return this._setInactiveState('disabled', 'NEOAGENT_SCREEN_RECORDER_ENABLED is not enabled');
121
+ }
122
+ if (this.platform !== 'darwin') {
123
+ return this._setInactiveState('unsupported', 'screen recording is currently supported only on macOS');
124
+ }
125
+
126
+ try {
127
+ this._loadConfiguration();
128
+ if (!this._ownerExists()) {
129
+ return this._setInactiveState(
130
+ 'misconfigured',
131
+ `configured screen recorder user ${this.ownerUserId} does not exist`,
132
+ );
133
+ }
134
+ } catch (err) {
135
+ return this._setInactiveState('misconfigured', getErrorMessage(err));
64
136
  }
65
137
 
66
- if (this.isRecording) return;
67
138
  this.isRecording = true;
139
+ this.state = 'running';
140
+ this.reason = null;
141
+ this.lastError = null;
142
+
143
+ this.intervalId = this.setInterval(() => {
144
+ void this.captureAndProcess();
145
+ }, this.intervalMs);
146
+ this.intervalId?.unref?.();
68
147
 
69
- console.log('[ScreenRecorder] Starting continuous screen recording (10s interval)');
70
-
71
- // Start the recording loop
72
- this.intervalId = setInterval(() => this.captureAndProcess(), this.intervalMs);
73
-
74
- // Run an initial capture
75
- this.captureAndProcess();
148
+ this.cleanupIntervalId = this.setInterval(
149
+ () => this.cleanupOldRecords(),
150
+ CLEANUP_INTERVAL_MS,
151
+ );
152
+ this.cleanupIntervalId?.unref?.();
76
153
 
77
- // Start daily cleanup of old records (7 days)
78
- this.cleanupIntervalId = setInterval(() => this.cleanupOldRecords(), 24 * 60 * 60 * 1000);
154
+ void this.captureAndProcess();
79
155
  this.cleanupOldRecords();
156
+ return this.getStatus();
80
157
  }
81
158
 
82
- stop() {
159
+ async stop() {
160
+ const wasRunning = this.isRecording;
83
161
  this.isRecording = false;
162
+
84
163
  if (this.intervalId) {
85
- clearInterval(this.intervalId);
164
+ this.clearInterval(this.intervalId);
86
165
  this.intervalId = null;
87
166
  }
88
167
  if (this.cleanupIntervalId) {
89
- clearInterval(this.cleanupIntervalId);
168
+ this.clearInterval(this.cleanupIntervalId);
90
169
  this.cleanupIntervalId = null;
91
170
  }
92
- console.log('[ScreenRecorder] Stopped continuous screen recording');
171
+
172
+ if (this.activeCapturePromise) {
173
+ await this.activeCapturePromise;
174
+ }
175
+ if (wasRunning) {
176
+ this.state = 'stopped';
177
+ this.reason = 'service stopped';
178
+ }
179
+ return this.getStatus();
180
+ }
181
+
182
+ _isCaptureInactiveApp(appName) {
183
+ const normalized = String(appName || '').trim().toLowerCase();
184
+ return normalized === '' || normalized === 'loginwindow' || normalized === 'screensaverengine';
185
+ }
186
+
187
+ _recordSkip(reason) {
188
+ this.lastSkipReason = reason;
189
+ const now = this.now();
190
+ if (now - this.lastBenignSkipAt < 5 * 60 * 1000) {
191
+ return;
192
+ }
193
+ this.lastBenignSkipAt = now;
194
+ console.log(`[ScreenRecorder] Capture skipped: ${reason}`);
93
195
  }
94
196
 
95
- async captureAndProcess() {
96
- if (this.isProcessing || !this.isRecording) return;
97
- this.isProcessing = true;
197
+ _recordError(err) {
198
+ this.lastError = getErrorMessage(err);
199
+ const now = this.now();
200
+ if (now - this.lastErrorLogAt < 5 * 60 * 1000) {
201
+ return;
202
+ }
203
+ this.lastErrorLogAt = now;
204
+ console.error('[ScreenRecorder] Capture/OCR failed:', this.lastError);
205
+ }
206
+
207
+ captureAndProcess() {
208
+ if (!this.isRecording) {
209
+ return Promise.resolve();
210
+ }
211
+ if (this.activeCapturePromise) {
212
+ return this.activeCapturePromise;
213
+ }
214
+
215
+ this.activeCapturePromise = this._captureAndProcess().finally(() => {
216
+ this.activeCapturePromise = null;
217
+ });
218
+ return this.activeCapturePromise;
219
+ }
98
220
 
221
+ async _captureAndProcess() {
222
+ this.lastCaptureAt = new Date(this.now()).toISOString();
99
223
  try {
100
- // Only capture while at least one external device/session is actively connected.
101
- // This prevents host-level screenshots when no user-side capture source is live.
102
- if (!this.hasActiveRemoteCaptureSession()) {
103
- this._logBenignSkip('no active external capture session');
224
+ if (!this.hasActiveCaptureSessionForUser(this.ownerUserId)) {
225
+ this._recordSkip('no active external capture session for the configured user');
104
226
  return;
105
227
  }
106
228
 
107
- // Skip capture when the desktop session is inactive (e.g. locked screen).
108
229
  let frontmostApp = '';
109
230
  try {
110
- const { stdout } = await execAsync(`osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'`);
111
- frontmostApp = (stdout || '').trim();
231
+ const { stdout } = await this.execFile('osascript', ['-e', FRONTMOST_APP_SCRIPT]);
232
+ frontmostApp = String(stdout || '').trim();
112
233
  } catch {
113
234
  frontmostApp = '';
114
235
  }
115
236
 
116
237
  if (this._isCaptureInactiveApp(frontmostApp)) {
117
- this._logBenignSkip('no active frontmost app');
238
+ this._recordSkip('no active frontmost app');
118
239
  return;
119
240
  }
120
241
 
121
- // Capture screen silently (-x) to file
122
- await execAsync(`screencapture -x "${this.tempFilePath}"`);
123
-
124
- // Verify file exists
125
- await fs.access(this.tempFilePath);
242
+ await this.execFile('screencapture', ['-x', this.tempFilePath]);
243
+ await this.fs.access(this.tempFilePath);
126
244
 
127
- // Extract text via local OCR
128
- const { data } = await tesseract.recognize(this.tempFilePath, 'eng+deu', {
129
- logger: () => {} // Silence verbose OCR logs
245
+ const { data } = await this.recognize(this.tempFilePath, 'eng+deu', {
246
+ logger: () => {},
130
247
  });
248
+ const textContent = String(data?.text || '').trim();
131
249
 
132
- const textContent = data.text.trim();
133
-
134
- // Only store if meaningful text was found
135
- if (textContent.length > 5) {
136
- // We need a user ID. For the local desktop agent, usually user 1 or we query the active user.
137
- const userRow = db.prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
138
- if (userRow) {
139
- // Identify the active foreground app via AppleScript
140
- let appName = frontmostApp || 'Unknown';
141
-
142
- db.prepare(`
143
- INSERT INTO screen_history (user_id, app_name, text_content)
144
- VALUES (?, ?, ?)
145
- `).run(userRow.id, appName, textContent);
146
- }
250
+ if (!this.isRecording || textContent.length <= MINIMUM_TEXT_LENGTH) {
251
+ return;
147
252
  }
148
253
 
254
+ this.db.prepare(`
255
+ INSERT INTO screen_history (user_id, app_name, text_content)
256
+ VALUES (?, ?, ?)
257
+ `).run(this.ownerUserId, frontmostApp, textContent);
258
+ this.lastSuccessAt = new Date(this.now()).toISOString();
259
+ this.lastSkipReason = null;
260
+ this.lastError = null;
149
261
  } catch (err) {
150
- const errorMessage = getErrorMessage(err);
151
- if (this._isBenignCaptureError(errorMessage)) {
152
- this._logBenignSkip(errorMessage);
153
- } else {
154
- console.error('[ScreenRecorder] Capture/OCR failed:', errorMessage);
155
- }
262
+ this._recordError(err);
156
263
  } finally {
157
- // Always cleanup the screenshot image immediately
158
264
  try {
159
- await fs.unlink(this.tempFilePath);
160
- } catch (e) {
161
- // Ignore unlink errors if file didn't exist
265
+ await this.fs.unlink(this.tempFilePath);
266
+ } catch {
267
+ // The file is absent when capture is skipped or fails before creating it.
162
268
  }
163
- this.isProcessing = false;
164
269
  }
165
270
  }
166
271
 
167
272
  cleanupOldRecords() {
168
273
  try {
169
- const result = db.prepare(`
170
- DELETE FROM screen_history
171
- WHERE timestamp < datetime('now', '-7 days')
172
- `).run();
274
+ const result = this.db.prepare(`
275
+ DELETE FROM screen_history
276
+ WHERE timestamp < datetime('now', ?)
277
+ `).run(`-${this.retentionDays} days`);
173
278
  if (result.changes > 0) {
174
279
  console.log(`[ScreenRecorder] Purged ${result.changes} old screen history records.`);
175
280
  }
@@ -179,4 +284,9 @@ class ScreenRecorder {
179
284
  }
180
285
  }
181
286
 
182
- module.exports = { ScreenRecorder };
287
+ module.exports = {
288
+ ScreenRecorder,
289
+ hasOpenConnectionForUser,
290
+ isExplicitlyEnabled,
291
+ parsePositiveInteger,
292
+ };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_CAPTURE_INTERVAL_MS = 10 * 1000;
4
+ const DEFAULT_RETENTION_DAYS = 7;
5
+ const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
6
+ const MIN_CAPTURE_INTERVAL_MS = 1000;
7
+ const MINIMUM_TEXT_LENGTH = 5;
8
+ const FRONTMOST_APP_SCRIPT = 'tell application "System Events" to get name of first application process whose frontmost is true';
9
+
10
+ function isExplicitlyEnabled(value) {
11
+ return ['1', 'true', 'on', 'yes'].includes(String(value || '').trim().toLowerCase());
12
+ }
13
+
14
+ function parsePositiveInteger(value, name, fallback, minimum = 1) {
15
+ if (value == null || String(value).trim() === '') {
16
+ return fallback;
17
+ }
18
+
19
+ const parsed = Number(value);
20
+ if (!Number.isSafeInteger(parsed) || parsed < minimum) {
21
+ throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
22
+ }
23
+ return parsed;
24
+ }
25
+
26
+ function hasOpenConnectionForUser(registry, userId) {
27
+ const connections = registry?.connectionsByUser?.get(String(userId));
28
+ if (connections instanceof Map) {
29
+ return Array.from(connections.values()).some(
30
+ (connection) => typeof connection?.isOpen === 'function' && connection.isOpen(),
31
+ );
32
+ }
33
+ return typeof connections?.isOpen === 'function' && connections.isOpen();
34
+ }
35
+
36
+ module.exports = {
37
+ CLEANUP_INTERVAL_MS,
38
+ DEFAULT_CAPTURE_INTERVAL_MS,
39
+ DEFAULT_RETENTION_DAYS,
40
+ FRONTMOST_APP_SCRIPT,
41
+ MINIMUM_TEXT_LENGTH,
42
+ MIN_CAPTURE_INTERVAL_MS,
43
+ hasOpenConnectionForUser,
44
+ isExplicitlyEnabled,
45
+ parsePositiveInteger,
46
+ };
@@ -24,7 +24,10 @@ const { WorkspaceManager } = require('./workspace/manager');
24
24
  const { BrowserExtensionRegistry } = require('./browser/extension/registry');
25
25
  const { DesktopCompanionRegistry } = require('./desktop/registry');
26
26
  const { DesktopProvider } = require('./desktop/provider');
27
- const { ScreenRecorder } = require('./desktop/screenRecorder');
27
+ const {
28
+ ScreenRecorder,
29
+ hasOpenConnectionForUser,
30
+ } = require('./desktop/screenRecorder');
28
31
  const { WearableService } = require('./wearable/service');
29
32
  const { getRuntimeValidation } = require('./runtime/validation');
30
33
  const {
@@ -112,10 +115,11 @@ function createMemoryIngestionService(app, { memoryManager, integrationManager }
112
115
  new MemoryIngestionService({
113
116
  memoryManager,
114
117
  integrationManager,
118
+ intervalMs: process.env.NEOAGENT_MEMORY_INGESTION_INTERVAL_MS || undefined,
115
119
  }),
116
120
  );
117
- memoryIngestionService.start();
118
- logServiceReady('Memory ingestion service started');
121
+ const status = memoryIngestionService.start();
122
+ logServiceReady(`Memory ingestion service started (${status.intervalMs}ms interval)`);
119
123
  return memoryIngestionService;
120
124
  }
121
125
 
@@ -368,44 +372,26 @@ function createWearableService(app) {
368
372
  }
369
373
 
370
374
  function createScreenRecorder(app) {
371
- const hasActiveRemoteCaptureSession = () => {
372
- const desktopRegistry = app.locals.desktopCompanionRegistry;
373
- if (desktopRegistry?.connectionsByUser instanceof Map) {
374
- for (const userMap of desktopRegistry.connectionsByUser.values()) {
375
- if (!(userMap instanceof Map)) continue;
376
- for (const connection of userMap.values()) {
377
- if (typeof connection?.isOpen === 'function' && connection.isOpen()) {
378
- return true;
379
- }
380
- }
381
- }
382
- }
383
-
384
- const extensionRegistry = app.locals.browserExtensionRegistry;
385
- if (extensionRegistry?.connectionsByUser instanceof Map) {
386
- for (const value of extensionRegistry.connectionsByUser.values()) {
387
- if (value instanceof Map) {
388
- for (const connection of value.values()) {
389
- if (typeof connection?.isOpen === 'function' && connection.isOpen()) {
390
- return true;
391
- }
392
- }
393
- } else if (typeof value?.isOpen === 'function' && value.isOpen()) {
394
- return true;
395
- }
396
- }
397
- }
398
-
399
- return false;
375
+ const hasActiveCaptureSessionForUser = (userId) => {
376
+ return (
377
+ hasOpenConnectionForUser(app.locals.desktopCompanionRegistry, userId)
378
+ || hasOpenConnectionForUser(app.locals.browserExtensionRegistry, userId)
379
+ );
400
380
  };
401
381
 
402
382
  const screenRecorder = registerLocal(
403
383
  app,
404
384
  'screenRecorder',
405
- new ScreenRecorder({ hasActiveRemoteCaptureSession }),
385
+ new ScreenRecorder({ hasActiveCaptureSessionForUser }),
406
386
  );
407
- screenRecorder.start();
408
- logServiceReady('Screen recorder started');
387
+ const status = screenRecorder.start();
388
+ if (status.state === 'running') {
389
+ logServiceReady(
390
+ `Screen recorder started for user ${status.ownerUserId} (${status.intervalMs}ms interval)`,
391
+ );
392
+ } else {
393
+ logServiceReady(`Screen recorder ${status.state}: ${status.reason}`);
394
+ }
409
395
  return screenRecorder;
410
396
  }
411
397
 
@@ -541,12 +527,16 @@ async function stopServices(app) {
541
527
  console.log('[Services] Stopping services');
542
528
 
543
529
  if (app.locals.taskRuntime) {
544
- try {
545
- app.locals.taskRuntime.stop();
546
- logServiceReady('Task runtime stopped');
547
- } catch (err) {
548
- console.error('[Tasks] Stop error:', getErrorMessage(err));
549
- }
530
+ tasks.push(
531
+ Promise.resolve()
532
+ .then(() => app.locals.taskRuntime.stop())
533
+ .then((status) => {
534
+ logServiceReady(`Task runtime shutdown complete (${status.state})`);
535
+ })
536
+ .catch((err) => {
537
+ console.error('[Tasks] Stop error:', getErrorMessage(err));
538
+ }),
539
+ );
550
540
  }
551
541
 
552
542
  if (app.locals.streamHub) {
@@ -559,12 +549,16 @@ async function stopServices(app) {
559
549
  }
560
550
 
561
551
  if (app.locals.memoryIngestionService) {
562
- try {
563
- app.locals.memoryIngestionService.stop();
564
- logServiceReady('Memory ingestion service stopped');
565
- } catch (err) {
566
- console.error('[MemoryIngestion] Stop error:', getErrorMessage(err));
567
- }
552
+ tasks.push(
553
+ Promise.resolve()
554
+ .then(() => app.locals.memoryIngestionService.stop())
555
+ .then((status) => {
556
+ logServiceReady(`Memory ingestion shutdown complete (${status.state})`);
557
+ })
558
+ .catch((err) => {
559
+ console.error('[MemoryIngestion] Stop error:', getErrorMessage(err));
560
+ }),
561
+ );
568
562
  }
569
563
 
570
564
  if (app.locals.mcpClient) {
@@ -637,12 +631,16 @@ async function stopServices(app) {
637
631
  }
638
632
 
639
633
  if (app.locals.screenRecorder) {
640
- try {
641
- app.locals.screenRecorder.stop();
642
- logServiceReady('Screen recorder stopped');
643
- } catch (err) {
644
- console.error('[ScreenRecorder] Stop error:', getErrorMessage(err));
645
- }
634
+ tasks.push(
635
+ Promise.resolve()
636
+ .then(() => app.locals.screenRecorder.stop())
637
+ .then((status) => {
638
+ logServiceReady(`Screen recorder shutdown complete (${status.state})`);
639
+ })
640
+ .catch((err) => {
641
+ console.error('[ScreenRecorder] Stop error:', getErrorMessage(err));
642
+ }),
643
+ );
646
644
  }
647
645
 
648
646
  if (app.locals.browserExtensionRegistry) {