neoagent 2.1.18-beta.93 → 2.1.18-beta.94

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.1.18-beta.93",
3
+ "version": "2.1.18-beta.94",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"59aa584fdf100e6c78c785d8a5b565d1de4b48
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "4149814514" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "769435206" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { TextDecoder } = require('util');
5
6
 
6
7
  function bufferToBase64Url(buffer) {
7
8
  return Buffer.from(buffer)
@@ -15,13 +16,125 @@ function stringToBase64Url(text) {
15
16
  return bufferToBase64Url(Buffer.from(String(text || ''), 'utf8'));
16
17
  }
17
18
 
18
- function base64UrlToString(value) {
19
- if (!value) return '';
20
- const normalized = String(value)
19
+ function base64UrlToBuffer(value) {
20
+ if (!value) return Buffer.alloc(0);
21
+ const raw = String(value);
22
+ const normalized = raw
21
23
  .replace(/-/g, '+')
22
24
  .replace(/_/g, '/')
23
- .padEnd(Math.ceil(String(value).length / 4) * 4, '=');
24
- return Buffer.from(normalized, 'base64').toString('utf8');
25
+ .padEnd(Math.ceil(raw.length / 4) * 4, '=');
26
+ return Buffer.from(normalized, 'base64');
27
+ }
28
+
29
+ function normalizeCharset(charset) {
30
+ const normalized = String(charset || 'utf-8').trim().toLowerCase();
31
+ if (!normalized) return 'utf-8';
32
+ if (normalized === 'utf8') return 'utf-8';
33
+ if (normalized === 'us-ascii') return 'ascii';
34
+ if (
35
+ normalized === 'latin1'
36
+ || normalized === 'latin-1'
37
+ || normalized === 'iso-8859-1'
38
+ || normalized === 'iso8859-1'
39
+ ) {
40
+ return 'windows-1252';
41
+ }
42
+ return normalized;
43
+ }
44
+
45
+ function decodeBytes(buffer, charset = 'utf-8') {
46
+ const bytes = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer || '');
47
+ const candidates = [
48
+ normalizeCharset(charset),
49
+ 'utf-8',
50
+ 'windows-1252',
51
+ ];
52
+ for (const candidate of candidates) {
53
+ try {
54
+ return new TextDecoder(candidate, { fatal: false }).decode(bytes);
55
+ } catch {}
56
+ }
57
+ return bytes.toString('utf8');
58
+ }
59
+
60
+ function mojibakeScore(value) {
61
+ const text = String(value || '');
62
+ let score = 0;
63
+ for (const char of text) {
64
+ if (char === '\uFFFD') score += 4;
65
+ else if (char === 'Ã' || char === 'Â') score += 3;
66
+ else if (char === 'â' || char === 'ð' || char === 'ï') score += 2;
67
+ }
68
+ return score;
69
+ }
70
+
71
+ function repairMojibake(value) {
72
+ let current = String(value || '');
73
+ for (let i = 0; i < 2; i += 1) {
74
+ if (mojibakeScore(current) === 0) break;
75
+ const repaired = Buffer.from(current, 'latin1').toString('utf8');
76
+ if (!repaired || mojibakeScore(repaired) >= mojibakeScore(current)) break;
77
+ current = repaired;
78
+ }
79
+ return current;
80
+ }
81
+
82
+ function base64UrlToString(value) {
83
+ return repairMojibake(decodeBytes(base64UrlToBuffer(value), 'utf-8'));
84
+ }
85
+
86
+ function parseCharset(value) {
87
+ const match = /charset\s*=\s*("?)([^";\s]+)\1/i.exec(String(value || ''));
88
+ return match?.[2] || 'utf-8';
89
+ }
90
+
91
+ function decodeQuotedPrintableToBuffer(value) {
92
+ const cleaned = String(value || '').replace(/=\r?\n/g, '');
93
+ const bytes = [];
94
+ for (let i = 0; i < cleaned.length; i += 1) {
95
+ const char = cleaned[i];
96
+ if (
97
+ char === '='
98
+ && i + 2 < cleaned.length
99
+ && /^[0-9A-Fa-f]{2}$/.test(cleaned.slice(i + 1, i + 3))
100
+ ) {
101
+ bytes.push(Number.parseInt(cleaned.slice(i + 1, i + 3), 16));
102
+ i += 2;
103
+ continue;
104
+ }
105
+ bytes.push(char.charCodeAt(0));
106
+ }
107
+ return Buffer.from(bytes);
108
+ }
109
+
110
+ function decodeMimeWord(charset, encoding, value) {
111
+ const normalizedEncoding = String(encoding || '').trim().toUpperCase();
112
+ if (normalizedEncoding === 'B') {
113
+ return repairMojibake(
114
+ decodeBytes(Buffer.from(String(value || ''), 'base64'), charset),
115
+ );
116
+ }
117
+ if (normalizedEncoding === 'Q') {
118
+ return repairMojibake(
119
+ decodeBytes(
120
+ decodeQuotedPrintableToBuffer(String(value || '').replace(/_/g, ' ')),
121
+ charset,
122
+ ),
123
+ );
124
+ }
125
+ return String(value || '');
126
+ }
127
+
128
+ function decodeMimeWords(value) {
129
+ return String(value || '').replace(
130
+ /=\?([^?]+)\?([bqBQ])\?([^?]*)\?=/g,
131
+ (_, charset, encoding, encodedText) =>
132
+ decodeMimeWord(charset, encoding, encodedText),
133
+ );
134
+ }
135
+
136
+ function normalizeDecodedText(value) {
137
+ return repairMojibake(decodeMimeWords(value));
25
138
  }
26
139
 
27
140
  function getHeader(headers, name) {
@@ -31,7 +144,7 @@ function getHeader(headers, name) {
31
144
  (header) => String(header?.name || '').toLowerCase() === normalized,
32
145
  )
33
146
  : null;
34
- return match?.value || null;
147
+ return match ? normalizeDecodedText(match.value) : null;
35
148
  }
36
149
 
37
150
  function stripHtml(html) {
@@ -45,11 +158,23 @@ function stripHtml(html) {
45
158
 
46
159
  function extractMessageBody(payload) {
47
160
  if (!payload) return '';
161
+ const headers = Array.isArray(payload.headers) ? payload.headers : [];
162
+ const contentType = getHeader(headers, 'Content-Type') || '';
163
+ const transferEncoding = (getHeader(headers, 'Content-Transfer-Encoding') || '').toLowerCase();
164
+ const charset = parseCharset(contentType);
48
165
  if (payload.body?.data && payload.mimeType === 'text/plain') {
49
- return base64UrlToString(payload.body.data);
166
+ const raw = base64UrlToBuffer(payload.body.data);
167
+ const decoded = transferEncoding.includes('quoted-printable')
168
+ ? decodeBytes(decodeQuotedPrintableToBuffer(raw.toString('latin1')), charset)
169
+ : decodeBytes(raw, charset);
170
+ return normalizeDecodedText(decoded);
50
171
  }
51
172
  if (payload.body?.data && payload.mimeType === 'text/html') {
52
- return stripHtml(base64UrlToString(payload.body.data));
173
+ const raw = base64UrlToBuffer(payload.body.data);
174
+ const decoded = transferEncoding.includes('quoted-printable')
175
+ ? decodeBytes(decodeQuotedPrintableToBuffer(raw.toString('latin1')), charset)
176
+ : decodeBytes(raw, charset);
177
+ return stripHtml(normalizeDecodedText(decoded));
53
178
  }
54
179
  if (!Array.isArray(payload.parts)) return '';
55
180
 
@@ -135,11 +260,13 @@ async function executeGoogleApiRequest(auth, args, options = {}) {
135
260
  }
136
261
 
137
262
  module.exports = {
263
+ base64UrlToBuffer,
138
264
  coerceStringList,
139
265
  ensureParentDir,
140
266
  executeGoogleApiRequest,
141
267
  extractMessageBody,
142
268
  getHeader,
269
+ normalizeDecodedText,
143
270
  stringToBase64Url,
144
271
  summarizeFile,
145
272
  };
@@ -6,6 +6,7 @@ const {
6
6
  executeGoogleApiRequest,
7
7
  extractMessageBody,
8
8
  getHeader,
9
+ normalizeDecodedText,
9
10
  stringToBase64Url,
10
11
  } = require('./common');
11
12
 
@@ -150,7 +151,7 @@ function summarizeMessage(message) {
150
151
  id: message.id,
151
152
  threadId: message.threadId,
152
153
  labelIds: Array.isArray(message.labelIds) ? message.labelIds : [],
153
- snippet: message.snippet || '',
154
+ snippet: normalizeDecodedText(message.snippet || ''),
154
155
  internalDate: message.internalDate
155
156
  ? new Date(Number(message.internalDate)).toISOString()
156
157
  : null,
@@ -218,7 +219,7 @@ async function executeGmailTool(toolName, args, auth) {
218
219
  : null;
219
220
  return {
220
221
  id: thread.id || '',
221
- snippet: thread.snippet || '',
222
+ snippet: normalizeDecodedText(thread.snippet || ''),
222
223
  historyId: thread.historyId || null,
223
224
  messageCount: Array.isArray(thread.messages)
224
225
  ? thread.messages.length
@@ -242,7 +243,7 @@ async function executeGmailTool(toolName, args, auth) {
242
243
  return {
243
244
  id: thread.id || '',
244
245
  historyId: thread.historyId || null,
245
- snippet: thread.snippet || '',
246
+ snippet: normalizeDecodedText(thread.snippet || ''),
246
247
  messages: (Array.isArray(thread.messages) ? thread.messages : []).map(
247
248
  summarizeMessage,
248
249
  ),
@@ -32,6 +32,7 @@ class DiscordPlatform extends BasePlatform {
32
32
 
33
33
  this._client = null;
34
34
  this._botUser = null;
35
+ this._manualDisconnect = false;
35
36
  }
36
37
 
37
38
  // ── Lifecycle ──────────────────────────────────────────────────────────────
@@ -39,6 +40,7 @@ class DiscordPlatform extends BasePlatform {
39
40
  async connect() {
40
41
  if (!this.token) throw new Error('Discord bot token is required');
41
42
 
43
+ this._manualDisconnect = false;
42
44
  if (this._client) { try { this._client.destroy(); } catch { } this._client = null; }
43
45
 
44
46
  this._client = new Client({
@@ -66,6 +68,7 @@ class DiscordPlatform extends BasePlatform {
66
68
  this._client.once('error', (err) => { clearTimeout(timeout); reject(err); });
67
69
  this._client.on('error', (err) => console.error('[Discord] Client error:', err.message));
68
70
  this._client.on('shardDisconnect', (event, shardId) => {
71
+ if (this._manualDisconnect) return;
69
72
  this.status = 'disconnected';
70
73
  console.warn(`[Discord] Shard ${shardId} disconnected (${event?.code || 'unknown'})`);
71
74
  this.emit('disconnected', {
@@ -76,15 +79,18 @@ class DiscordPlatform extends BasePlatform {
76
79
  });
77
80
  });
78
81
  this._client.on('shardReconnecting', (shardId) => {
82
+ if (this._manualDisconnect) return;
79
83
  this.status = 'connecting';
80
84
  console.log(`[Discord] Shard ${shardId} reconnecting`);
81
85
  });
82
86
  this._client.on('shardResume', (_replayedEvents, shardId) => {
87
+ if (this._manualDisconnect) return;
83
88
  this.status = 'connected';
84
89
  console.log(`[Discord] Shard ${shardId} resumed`);
85
90
  this.emit('connected');
86
91
  });
87
92
  this._client.on('invalidated', () => {
93
+ if (this._manualDisconnect) return;
88
94
  this.status = 'logged_out';
89
95
  console.warn('[Discord] Session invalidated');
90
96
  this.emit('logged_out');
@@ -96,6 +102,7 @@ class DiscordPlatform extends BasePlatform {
96
102
  }
97
103
 
98
104
  async disconnect() {
105
+ this._manualDisconnect = true;
99
106
  if (this._client) { try { this._client.destroy(); } catch { } this._client = null; }
100
107
  this.status = 'disconnected';
101
108
  this._botUser = null;
@@ -107,6 +107,10 @@ class MessagingManager extends EventEmitter {
107
107
  }
108
108
 
109
109
  async ingestMessage(userId, platformName, msg, options = {}) {
110
+ if (this.isShuttingDown) {
111
+ return null;
112
+ }
113
+
110
114
  const agentId = this._agentId(userId, {
111
115
  ...options,
112
116
  agentId: options?.agentId ?? msg?.agentId ?? null,
@@ -136,9 +140,16 @@ class MessagingManager extends EventEmitter {
136
140
 
137
141
  const enrichedMsg = { ...msg, agentId, platform: platformName };
138
142
 
143
+ if (this.isShuttingDown) {
144
+ return enrichedMsg;
145
+ }
146
+
139
147
  this.io.to(`user:${userId}`).emit('messaging:message', enrichedMsg);
140
148
 
141
149
  for (const handler of this.messageHandlers) {
150
+ if (this.isShuttingDown) {
151
+ break;
152
+ }
142
153
  try {
143
154
  await handler(userId, enrichedMsg);
144
155
  } catch (err) {
@@ -332,14 +343,14 @@ class MessagingManager extends EventEmitter {
332
343
  const currentPlatform = () => this.platforms.get(key) === platform;
333
344
 
334
345
  platform.on('qr', (qr) => {
335
- if (!currentPlatform()) return;
346
+ if (!currentPlatform() || this.isShuttingDown) return;
336
347
  this.io.to(`user:${userId}`).emit('messaging:qr', { agentId, platform: platformName, qr });
337
348
  db.prepare('UPDATE platform_connections SET status = ?, config = ? WHERE user_id = ? AND agent_id = ? AND platform = ?')
338
349
  .run('awaiting_qr', storedConfig, userId, agentId, platformName);
339
350
  });
340
351
 
341
352
  platform.on('connected', () => {
342
- if (!currentPlatform()) return;
353
+ if (!currentPlatform() || this.isShuttingDown) return;
343
354
  this.io.to(`user:${userId}`).emit('messaging:connected', { agentId, platform: platformName });
344
355
  db.prepare('UPDATE platform_connections SET status = ?, last_connected = datetime(\'now\') WHERE user_id = ? AND agent_id = ? AND platform = ?')
345
356
  .run('connected', userId, agentId, platformName);
@@ -355,7 +366,7 @@ class MessagingManager extends EventEmitter {
355
366
  });
356
367
 
357
368
  platform.on('logged_out', () => {
358
- if (!currentPlatform()) return;
369
+ if (!currentPlatform() || this.isShuttingDown) return;
359
370
  this.io.to(`user:${userId}`).emit('messaging:logged_out', { agentId, platform: platformName });
360
371
  db.prepare('UPDATE platform_connections SET status = ? WHERE user_id = ? AND agent_id = ? AND platform = ?')
361
372
  .run('logged_out', userId, agentId, platformName);
@@ -385,6 +396,7 @@ class MessagingManager extends EventEmitter {
385
396
  });
386
397
 
387
398
  platform.on('message', async (msg) => {
399
+ if (this.isShuttingDown) return;
388
400
  await this.ingestMessage(userId, platformName, msg, { agentId });
389
401
  });
390
402
 
@@ -16,6 +16,8 @@ class WhatsAppPlatform extends BasePlatform {
16
16
  this.reconnectAttempts = 0;
17
17
  this.maxReconnect = 5;
18
18
  this.authDir = config.authDir || AUTH_DIR;
19
+ this._manualDisconnect = false;
20
+ this._reconnectTimer = null;
19
21
  }
20
22
 
21
23
  _ownIds() {
@@ -53,6 +55,11 @@ class WhatsAppPlatform extends BasePlatform {
53
55
  }
54
56
 
55
57
  async connect() {
58
+ this._manualDisconnect = false;
59
+ if (this._reconnectTimer) {
60
+ clearTimeout(this._reconnectTimer);
61
+ this._reconnectTimer = null;
62
+ }
56
63
  if (!fs.existsSync(this.authDir)) fs.mkdirSync(this.authDir, { recursive: true });
57
64
 
58
65
  const {
@@ -108,15 +115,21 @@ class WhatsAppPlatform extends BasePlatform {
108
115
 
109
116
  if (connection === 'close') {
110
117
  const statusCode = lastDisconnect?.error?.output?.statusCode;
111
- const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
118
+ const shouldReconnect = !this._manualDisconnect && statusCode !== DisconnectReason.loggedOut;
112
119
 
113
120
  this.status = 'disconnected';
114
- this.emit('disconnected', { statusCode, shouldReconnect });
121
+ this.emit('disconnected', { statusCode, shouldReconnect, manual: this._manualDisconnect });
115
122
 
116
123
  if (shouldReconnect && this.reconnectAttempts < this.maxReconnect) {
117
124
  this.reconnectAttempts++;
118
125
  const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
119
- setTimeout(() => this.connect(), delay);
126
+ this._reconnectTimer = setTimeout(() => {
127
+ this._reconnectTimer = null;
128
+ if (this._manualDisconnect) return;
129
+ this.connect().catch((err) => {
130
+ console.error('[WhatsApp] Reconnect failed:', err.message);
131
+ });
132
+ }, delay);
120
133
  } else if (statusCode === DisconnectReason.loggedOut) {
121
134
  fs.rmSync(this.authDir, { recursive: true, force: true });
122
135
  this.emit('logged_out');
@@ -234,6 +247,11 @@ class WhatsAppPlatform extends BasePlatform {
234
247
  }
235
248
 
236
249
  async disconnect() {
250
+ this._manualDisconnect = true;
251
+ if (this._reconnectTimer) {
252
+ clearTimeout(this._reconnectTimer);
253
+ this._reconnectTimer = null;
254
+ }
237
255
  if (this.sock) {
238
256
  this.sock.end();
239
257
  this.sock = null;
@@ -328,6 +346,11 @@ class WhatsAppPlatform extends BasePlatform {
328
346
  }
329
347
 
330
348
  async logout() {
349
+ this._manualDisconnect = true;
350
+ if (this._reconnectTimer) {
351
+ clearTimeout(this._reconnectTimer);
352
+ this._reconnectTimer = null;
353
+ }
331
354
  if (this.sock) {
332
355
  await this.sock.logout();
333
356
  this.sock = null;
@@ -807,6 +807,11 @@ function setupWebSocket(io, services) {
807
807
  // ── Disconnect ──
808
808
 
809
809
  socket.on('disconnect', () => {
810
+ if (!voiceRuntimeManager || typeof voiceRuntimeManager.closeSession !== 'function') {
811
+ socket.data.voiceSessionIds?.clear?.();
812
+ console.log(`[WS] User ${userId} disconnected (${socket.id})`);
813
+ return;
814
+ }
810
815
  const activeVoiceSessionIds = Array.from(socket.data.voiceSessionIds || []);
811
816
  for (const sessionId of activeVoiceSessionIds) {
812
817
  void voiceRuntimeManager.closeSession(sessionId, 'socket_disconnected').catch((err) => {