neoagent 2.1.18-beta.92 → 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.92",
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: "2746919848" /* 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
 
@@ -18,6 +18,7 @@ class TelegramPlatform extends BasePlatform {
18
18
  this._botUser = null;
19
19
  this._contextBuffers = new Map();
20
20
  this._contextMaxSize = 25;
21
+ this._contextMaxChats = 200;
21
22
  }
22
23
 
23
24
  async connect() {
@@ -89,6 +90,7 @@ class TelegramPlatform extends BasePlatform {
89
90
  }
90
91
  this.status = 'disconnected';
91
92
  this._botUser = null;
93
+ this._contextBuffers.clear();
92
94
  this.emit('disconnected', { manual: true });
93
95
  }
94
96
 
@@ -158,6 +160,16 @@ class TelegramPlatform extends BasePlatform {
158
160
  const buf = this._contextBuffers.get(rawChatId);
159
161
  buf.push(entry);
160
162
  if (buf.length > this._contextMaxSize) buf.shift();
163
+
164
+ // Keep only the most recently active chat contexts so memory usage does not
165
+ // grow forever as the bot encounters new chats/groups over time.
166
+ this._contextBuffers.delete(rawChatId);
167
+ this._contextBuffers.set(rawChatId, buf);
168
+ while (this._contextBuffers.size > this._contextMaxChats) {
169
+ const oldestKey = this._contextBuffers.keys().next().value;
170
+ if (oldestKey == null) break;
171
+ this._contextBuffers.delete(oldestKey);
172
+ }
161
173
  }
162
174
 
163
175
  _getContext(rawChatId) {
@@ -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;
@@ -49,11 +49,17 @@ class BufferedLiveRelayAdapter {
49
49
  if (!audioBytes.length) {
50
50
  return '';
51
51
  }
52
- return this._transcribeAudioSnapshot(audioBytes, session.inputMimeType, {
53
- model: session.voiceSettings?.liveSttModel,
54
- userId: session.userId,
55
- agentId: session.agentId,
56
- });
52
+ try {
53
+ return await this._transcribeAudioSnapshot(audioBytes, session.inputMimeType, {
54
+ model: session.voiceSettings?.liveSttModel,
55
+ userId: session.userId,
56
+ agentId: session.agentId,
57
+ });
58
+ } finally {
59
+ // Release buffered audio immediately after commit so completed turns do
60
+ // not retain large input chunks until the next turn or explicit close.
61
+ session.resetInput(session.inputMimeType);
62
+ }
57
63
  }
58
64
 
59
65
  _schedulePartialTranscript(session) {
@@ -485,12 +485,16 @@ function setupWebSocket(io, services) {
485
485
  const data = asObject(raw);
486
486
  const agentId = resolveAgentFromPayload(userId, data);
487
487
  const sessionId = toOptionalString(data?.sessionId, 128);
488
- await voiceRuntimeManager.openFlutterSession({
488
+ const session = await voiceRuntimeManager.openFlutterSession({
489
489
  userId,
490
490
  agentId,
491
491
  socket,
492
492
  sessionId: sessionId || null,
493
493
  });
494
+ if (!socket.data.voiceSessionIds) {
495
+ socket.data.voiceSessionIds = new Set();
496
+ }
497
+ socket.data.voiceSessionIds.add(session.id);
494
498
  } catch (err) {
495
499
  socket.emit('voice:error', { error: sanitizeError(err) });
496
500
  }
@@ -665,6 +669,7 @@ function setupWebSocket(io, services) {
665
669
  return socket.emit('voice:error', { error: 'sessionId is required' });
666
670
  }
667
671
  await voiceRuntimeManager.closeSession(sessionId, 'client_closed');
672
+ socket.data.voiceSessionIds?.delete(sessionId);
668
673
  } catch (err) {
669
674
  console.error(`[WS] voice:session_close failed for user ${userId}:`, err);
670
675
  socket.emit('voice:error', {
@@ -802,6 +807,18 @@ function setupWebSocket(io, services) {
802
807
  // ── Disconnect ──
803
808
 
804
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
+ }
815
+ const activeVoiceSessionIds = Array.from(socket.data.voiceSessionIds || []);
816
+ for (const sessionId of activeVoiceSessionIds) {
817
+ void voiceRuntimeManager.closeSession(sessionId, 'socket_disconnected').catch((err) => {
818
+ console.error(`[WS] Failed to close voice session ${sessionId} after socket disconnect:`, err);
819
+ });
820
+ }
821
+ socket.data.voiceSessionIds?.clear?.();
805
822
  console.log(`[WS] User ${userId} disconnected (${socket.id})`);
806
823
  });
807
824
  });