neoagent 3.2.1-beta.2 → 3.2.1-beta.4

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 (145) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +347 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +8 -41
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +15 -28
  52. package/server/services/ai/providers/githubCopilot.js +36 -16
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +13 -22
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +102 -82
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/messaging/whatsapp.js +22 -14
  116. package/server/services/network/http.js +210 -0
  117. package/server/services/network/safe_request.js +307 -0
  118. package/server/services/runtime/backends/local-vm.js +214 -66
  119. package/server/services/runtime/manager.js +17 -12
  120. package/server/services/social_reach/channels/github.js +10 -4
  121. package/server/services/social_reach/channels/reddit.js +4 -4
  122. package/server/services/social_reach/channels/rss.js +2 -2
  123. package/server/services/social_reach/channels/social_video.js +12 -7
  124. package/server/services/social_reach/channels/v2ex.js +21 -8
  125. package/server/services/social_reach/channels/x.js +2 -2
  126. package/server/services/social_reach/channels/xueqiu.js +5 -5
  127. package/server/services/social_reach/service.js +9 -6
  128. package/server/services/social_reach/utils.js +65 -14
  129. package/server/services/social_video/service.js +160 -50
  130. package/server/services/tasks/integration_runtime.js +18 -8
  131. package/server/services/tasks/runtime.js +39 -4
  132. package/server/services/voice/agentBridge.js +17 -4
  133. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  134. package/server/services/voice/liveSession.js +31 -0
  135. package/server/services/voice/openaiSpeech.js +33 -8
  136. package/server/services/voice/providers.js +233 -151
  137. package/server/services/voice/runtimeManager.js +118 -20
  138. package/server/services/voice/turnRunner.js +6 -0
  139. package/server/services/wearable/firmware_manifest.js +51 -13
  140. package/server/services/wearable/service.js +1 -0
  141. package/server/utils/abort.js +96 -0
  142. package/server/utils/cloud-security.js +110 -3
  143. package/server/utils/files.js +31 -0
  144. package/server/utils/image_payload.js +95 -0
  145. package/server/utils/retry.js +107 -0
@@ -566,7 +566,27 @@ async function stopServices(app) {
566
566
  console.error('[ApprovalGate] Shutdown error:', getErrorMessage(err));
567
567
  }
568
568
  }
569
- if (app.locals.agentEngine && typeof app.locals.agentEngine.interruptAllActiveRuns === 'function') {
569
+ if (app.locals.agentEngine && typeof app.locals.agentEngine.shutdown === 'function') {
570
+ try {
571
+ tasks.push(
572
+ app.locals.agentEngine.shutdown().then((status) => {
573
+ if (status.timedOut) {
574
+ console.warn(
575
+ `[AgentEngine] Shutdown timed out with ${status.pendingCount} operation(s) still settling`,
576
+ );
577
+ } else {
578
+ logServiceReady('Agent engine shutdown complete');
579
+ }
580
+ }),
581
+ );
582
+ logServiceReady('Active runs and background work marked interrupted');
583
+ } catch (err) {
584
+ console.error('[AgentEngine] Shutdown error:', getErrorMessage(err));
585
+ }
586
+ } else if (
587
+ app.locals.agentEngine
588
+ && typeof app.locals.agentEngine.interruptAllActiveRuns === 'function'
589
+ ) {
570
590
  try {
571
591
  app.locals.agentEngine.interruptAllActiveRuns();
572
592
  logServiceReady('Active runs marked interrupted');
@@ -595,6 +615,23 @@ async function stopServices(app) {
595
615
  );
596
616
  }
597
617
 
618
+ if (app.locals.messagingAutomationRuntime) {
619
+ tasks.push(
620
+ Promise.resolve()
621
+ .then(() => app.locals.messagingAutomationRuntime.shutdown())
622
+ .then((status) => {
623
+ if (status.timedOut) {
624
+ console.warn('[MessagingAutomation] Shutdown timed out while work was settling');
625
+ } else {
626
+ logServiceReady('Messaging automation shutdown complete');
627
+ }
628
+ })
629
+ .catch((err) => {
630
+ console.error('[MessagingAutomation] Shutdown error:', getErrorMessage(err));
631
+ }),
632
+ );
633
+ }
634
+
598
635
  if (app.locals.streamHub) {
599
636
  try {
600
637
  await app.locals.streamHub.shutdown();
@@ -617,6 +654,22 @@ async function stopServices(app) {
617
654
  );
618
655
  }
619
656
 
657
+ if (
658
+ app.locals.integrationManager &&
659
+ typeof app.locals.integrationManager.shutdown === 'function'
660
+ ) {
661
+ tasks.push(
662
+ Promise.resolve()
663
+ .then(() => app.locals.integrationManager.shutdown())
664
+ .then((status) => {
665
+ logServiceReady(`Official integrations shutdown complete (${status.state})`);
666
+ })
667
+ .catch((err) => {
668
+ console.error('[Integrations] Shutdown error:', getErrorMessage(err));
669
+ }),
670
+ );
671
+ }
672
+
620
673
  if (app.locals.mcpClient) {
621
674
  tasks.push(
622
675
  app.locals.mcpClient.shutdown().catch((err) => {
@@ -641,6 +694,22 @@ async function stopServices(app) {
641
694
  );
642
695
  }
643
696
 
697
+ if (app.locals.browserExtensionGateway?.close) {
698
+ tasks.push(
699
+ app.locals.browserExtensionGateway.close().catch((err) => {
700
+ console.error('[BrowserExtensionGateway] Shutdown error:', getErrorMessage(err));
701
+ }),
702
+ );
703
+ }
704
+
705
+ if (app.locals.desktopCompanionGateway?.close) {
706
+ tasks.push(
707
+ app.locals.desktopCompanionGateway.close().catch((err) => {
708
+ console.error('[DesktopCompanionGateway] Shutdown error:', getErrorMessage(err));
709
+ }),
710
+ );
711
+ }
712
+
644
713
  if (app.locals.browserControllers instanceof Map) {
645
714
  for (const controller of app.locals.browserControllers.values()) {
646
715
  tasks.push(
@@ -659,6 +728,23 @@ async function stopServices(app) {
659
728
  );
660
729
  }
661
730
 
731
+ if (
732
+ app.locals.voiceRuntimeManager
733
+ && typeof app.locals.voiceRuntimeManager.shutdown === 'function'
734
+ ) {
735
+ tasks.push(
736
+ app.locals.voiceRuntimeManager.shutdown().then((status) => {
737
+ if (status.timedOut) {
738
+ console.warn('[VoiceRuntime] Shutdown timed out while sessions were closing');
739
+ } else {
740
+ logServiceReady('Voice runtime shutdown complete');
741
+ }
742
+ }).catch((err) => {
743
+ console.error('[VoiceRuntime] Shutdown error:', getErrorMessage(err));
744
+ }),
745
+ );
746
+ }
747
+
662
748
  if (app.locals.runtimeManager) {
663
749
  tasks.push(
664
750
  app.locals.runtimeManager.shutdown().catch((err) => {
@@ -95,6 +95,8 @@ function findEmbeddingCandidates(db, {
95
95
  userId,
96
96
  agentId,
97
97
  embedding,
98
+ embeddingProvider = null,
99
+ embeddingModel = null,
98
100
  limit = DEFAULT_CANDIDATE_LIMIT,
99
101
  }) {
100
102
  const vector = typeof embedding === 'string'
@@ -104,17 +106,25 @@ function findEmbeddingCandidates(db, {
104
106
  if (!probesByBand.length) return [];
105
107
 
106
108
  const matches = new Map();
109
+ const embeddingSpace = embeddingProvider && embeddingModel
110
+ ? `${embeddingProvider}:${embeddingModel}`
111
+ : '';
107
112
  for (const band of probesByBand) {
108
113
  const placeholders = band.values.map(() => '?').join(', ');
109
114
  const rows = db.prepare(
110
- `SELECT memory_id
111
- FROM memory_embedding_bands
112
- WHERE user_id = ?
113
- AND agent_id = ?
114
- AND dimension = ?
115
- AND index_version = ?
116
- AND band_index = ?
117
- AND band_value IN (${placeholders})`
115
+ `SELECT bands.memory_id
116
+ FROM memory_embedding_bands bands
117
+ JOIN memories memory ON memory.id = bands.memory_id
118
+ WHERE bands.user_id = ?
119
+ AND bands.agent_id = ?
120
+ AND bands.dimension = ?
121
+ AND bands.index_version = ?
122
+ AND bands.band_index = ?
123
+ AND bands.band_value IN (${placeholders})
124
+ AND (
125
+ ? = ''
126
+ OR (memory.embedding_provider || ':' || memory.embedding_model) = ?
127
+ )`
118
128
  ).all(
119
129
  userId,
120
130
  agentId || '',
@@ -122,6 +132,8 @@ function findEmbeddingCandidates(db, {
122
132
  INDEX_VERSION,
123
133
  band.bandIndex,
124
134
  ...band.values,
135
+ embeddingSpace,
136
+ embeddingSpace,
125
137
  );
126
138
  for (const row of rows) {
127
139
  matches.set(row.memory_id, (matches.get(row.memory_id) || 0) + 1);
@@ -4,134 +4,192 @@
4
4
  * Embedding helpers for the semantic memory system.
5
5
  *
6
6
  * Provider selection (in priority order):
7
- * 1. Google (text-embedding-004, 768 dims) — when provider hint is 'google' and GOOGLE_AI_KEY is set
7
+ * 1. Google (gemini-embedding-2, 768 dims) — when provider hint is 'google' and GOOGLE_AI_KEY is set
8
8
  * 2. OpenAI (text-embedding-3-small, 1536 dims) — when OPENAI_API_KEY is set
9
9
  * 3. Keyword fallback — when no API key is available
10
10
  */
11
11
 
12
12
  const https = require('https');
13
+ const {
14
+ createAbortError,
15
+ isAbortError,
16
+ throwIfAborted,
17
+ } = require('../../utils/abort');
13
18
 
14
19
  const OPENAI_MODEL = 'text-embedding-3-small';
15
20
  const OPENAI_DIM = 1536;
16
- const GOOGLE_MODEL = 'text-embedding-004';
21
+ const GOOGLE_MODEL = 'gemini-embedding-2';
17
22
  const GOOGLE_DIM = 768;
23
+ const EMBEDDING_TIMEOUT_MS = 15000;
24
+ const MAX_EMBEDDING_RESPONSE_BYTES = 2 * 1024 * 1024;
18
25
 
19
26
  // Exported so callers can sanity-check stored vector dimensions if needed
20
27
  const EMBED_DIM = OPENAI_DIM;
21
28
  const EMBED_DIM_GOOGLE = GOOGLE_DIM;
22
29
 
23
- async function getGeminiEmbedding(text) {
24
- const apiKey = process.env.GOOGLE_AI_KEY;
25
- if (!apiKey) return null;
26
- if (!text || !text.trim()) return null;
27
-
28
- const truncated = text.slice(0, 25000);
30
+ function toEmbeddingVector(value, expectedDimensions) {
31
+ if (!Array.isArray(value) || value.length !== expectedDimensions) return null;
32
+ const vector = new Float32Array(value.length);
33
+ for (let index = 0; index < value.length; index += 1) {
34
+ const number = Number(value[index]);
35
+ if (!Number.isFinite(number)) return null;
36
+ vector[index] = number;
37
+ }
38
+ return vector;
39
+ }
29
40
 
30
- return new Promise((resolve) => {
41
+ function requestEmbeddingJson({ hostname, path, headers, body, signal }) {
42
+ throwIfAborted(signal, 'Embedding request aborted.');
43
+ return new Promise((resolve, reject) => {
31
44
  let settled = false;
32
- const done = (value) => {
45
+ let request = null;
46
+ let response = null;
47
+ let timer = null;
48
+
49
+ const cleanup = () => {
50
+ if (timer) clearTimeout(timer);
51
+ signal?.removeEventListener('abort', onAbort);
52
+ };
53
+ const finish = (error, value = null) => {
33
54
  if (settled) return;
34
55
  settled = true;
35
- resolve(value);
56
+ cleanup();
57
+ if (error) reject(error);
58
+ else resolve(value);
59
+ };
60
+ const onAbort = () => {
61
+ const error = createAbortError(signal, 'Embedding request aborted.');
62
+ response?.destroy(error);
63
+ request?.destroy(error);
64
+ finish(error);
36
65
  };
66
+ signal?.addEventListener('abort', onAbort, { once: true });
37
67
 
38
- const body = JSON.stringify({
39
- model: `models/${GOOGLE_MODEL}`,
40
- content: { parts: [{ text: truncated }] }
41
- });
68
+ timer = setTimeout(() => {
69
+ request?.destroy();
70
+ response?.destroy();
71
+ finish(null, null);
72
+ }, EMBEDDING_TIMEOUT_MS);
42
73
 
43
- const path = `/v1beta/models/${GOOGLE_MODEL}:embedContent?key=${apiKey}`;
44
- const options = {
45
- hostname: 'generativelanguage.googleapis.com',
74
+ request = https.request({
75
+ hostname,
46
76
  path,
47
77
  method: 'POST',
48
78
  headers: {
79
+ ...headers,
49
80
  'Content-Type': 'application/json',
50
- 'Content-Length': Buffer.byteLength(body)
81
+ 'Content-Length': Buffer.byteLength(body),
82
+ },
83
+ }, (incoming) => {
84
+ response = incoming;
85
+ if (settled) {
86
+ incoming.destroy();
87
+ return;
88
+ }
89
+ if (incoming.statusCode < 200 || incoming.statusCode >= 300) {
90
+ incoming.destroy();
91
+ finish(null, null);
92
+ return;
93
+ }
94
+ const contentLength = Number(incoming.headers?.['content-length']);
95
+ if (
96
+ Number.isFinite(contentLength)
97
+ && contentLength > MAX_EMBEDDING_RESPONSE_BYTES
98
+ ) {
99
+ incoming.destroy();
100
+ finish(null, null);
101
+ return;
51
102
  }
52
- };
53
103
 
54
- const req = https.request(options, (res) => {
55
- let data = '';
56
- res.on('data', chunk => { data += chunk; });
57
- res.on('end', () => {
104
+ const chunks = [];
105
+ let totalBytes = 0;
106
+ incoming.on('data', (chunk) => {
107
+ if (settled) return;
108
+ const buffer = Buffer.from(chunk);
109
+ totalBytes += buffer.byteLength;
110
+ if (totalBytes > MAX_EMBEDDING_RESPONSE_BYTES) {
111
+ incoming.destroy();
112
+ finish(null, null);
113
+ return;
114
+ }
115
+ chunks.push(buffer);
116
+ });
117
+ incoming.on('end', () => {
118
+ if (settled) return;
58
119
  try {
59
- const parsed = JSON.parse(data);
60
- const vec = parsed.embedding?.values;
61
- if (!vec) return done(null);
62
- done(new Float32Array(vec));
120
+ finish(null, JSON.parse(Buffer.concat(chunks, totalBytes).toString('utf8')));
63
121
  } catch {
64
- done(null);
122
+ finish(null, null);
65
123
  }
66
124
  });
125
+ incoming.on('error', (error) => {
126
+ if (isAbortError(error, signal)) finish(createAbortError(signal));
127
+ else finish(null, null);
128
+ });
129
+ incoming.on('aborted', () => {
130
+ if (signal?.aborted) finish(createAbortError(signal));
131
+ else finish(null, null);
132
+ });
67
133
  });
68
134
 
69
- req.on('error', () => done(null));
70
- req.setTimeout(15000, () => {
71
- req.destroy();
72
- done(null);
135
+ request.on('error', (error) => {
136
+ if (isAbortError(error, signal)) finish(createAbortError(signal));
137
+ else finish(null, null);
73
138
  });
74
- req.write(body);
75
- req.end();
139
+ request.end(body);
76
140
  });
77
141
  }
78
142
 
79
- async function getOpenAIEmbedding(text) {
80
- const apiKey = process.env.OPENAI_API_KEY;
143
+ function formatGoogleEmbeddingInput(text, inputType) {
144
+ if (inputType === 'query') return `task: search result | query: ${text}`;
145
+ if (inputType === 'document') return `title: none | text: ${text}`;
146
+ return `task: sentence similarity | query: ${text}`;
147
+ }
148
+
149
+ async function getGeminiEmbedding(text, options = {}) {
150
+ const apiKey = process.env.GOOGLE_AI_KEY;
81
151
  if (!apiKey) return null;
82
152
  if (!text || !text.trim()) return null;
83
153
 
84
- const truncated = text.slice(0, 25000);
85
-
86
- return new Promise((resolve) => {
87
- let settled = false;
88
- const done = (value) => {
89
- if (settled) return;
90
- settled = true;
91
- resolve(value);
92
- };
93
-
94
- const body = JSON.stringify({
95
- model: OPENAI_MODEL,
96
- input: truncated,
97
- encoding_format: 'float'
98
- });
154
+ const truncated = formatGoogleEmbeddingInput(
155
+ text.slice(0, 25000),
156
+ options.inputType,
157
+ );
158
+ const body = JSON.stringify({
159
+ model: `models/${GOOGLE_MODEL}`,
160
+ content: { parts: [{ text: truncated }] },
161
+ output_dimensionality: GOOGLE_DIM,
162
+ });
163
+ const data = await requestEmbeddingJson({
164
+ hostname: 'generativelanguage.googleapis.com',
165
+ path: `/v1beta/models/${GOOGLE_MODEL}:embedContent`,
166
+ headers: { 'x-goog-api-key': apiKey },
167
+ body,
168
+ signal: options.signal,
169
+ });
170
+ return toEmbeddingVector(data?.embedding?.values, GOOGLE_DIM);
171
+ }
99
172
 
100
- const options = {
101
- hostname: 'api.openai.com',
102
- path: '/v1/embeddings',
103
- method: 'POST',
104
- headers: {
105
- 'Authorization': `Bearer ${apiKey}`,
106
- 'Content-Type': 'application/json',
107
- 'Content-Length': Buffer.byteLength(body)
108
- }
109
- };
173
+ async function getOpenAIEmbedding(text, options = {}) {
174
+ const apiKey = process.env.OPENAI_API_KEY;
175
+ if (!apiKey) return null;
176
+ if (!text || !text.trim()) return null;
110
177
 
111
- const req = https.request(options, (res) => {
112
- let data = '';
113
- res.on('data', chunk => { data += chunk; });
114
- res.on('end', () => {
115
- try {
116
- const parsed = JSON.parse(data);
117
- if (parsed.error) return done(null);
118
- const vec = parsed.data?.[0]?.embedding;
119
- if (!vec) return done(null);
120
- done(new Float32Array(vec));
121
- } catch {
122
- done(null);
123
- }
124
- });
125
- });
178
+ const truncated = text.slice(0, 25000);
126
179
 
127
- req.on('error', () => done(null));
128
- req.setTimeout(15000, () => {
129
- req.destroy();
130
- done(null);
131
- });
132
- req.write(body);
133
- req.end();
180
+ const body = JSON.stringify({
181
+ model: OPENAI_MODEL,
182
+ input: truncated,
183
+ encoding_format: 'float',
184
+ });
185
+ const data = await requestEmbeddingJson({
186
+ hostname: 'api.openai.com',
187
+ path: '/v1/embeddings',
188
+ headers: { Authorization: `Bearer ${apiKey}` },
189
+ body,
190
+ signal: options.signal,
134
191
  });
192
+ return toEmbeddingVector(data?.data?.[0]?.embedding, OPENAI_DIM);
135
193
  }
136
194
 
137
195
  /**
@@ -140,15 +198,16 @@ async function getOpenAIEmbedding(text) {
140
198
  * @param {string} [provider] - 'google' to prefer Gemini embeddings
141
199
  * @returns {Float32Array|null}
142
200
  */
143
- async function getEmbedding(text, provider) {
144
- const result = await getEmbeddingWithMetadata(text, provider);
201
+ async function getEmbedding(text, provider, options = {}) {
202
+ const result = await getEmbeddingWithMetadata(text, provider, options);
145
203
  return result?.vector || null;
146
204
  }
147
205
 
148
- async function getEmbeddingWithMetadata(text, provider) {
206
+ async function getEmbeddingWithMetadata(text, provider, options = {}) {
149
207
  if (!text || !text.trim()) return null;
208
+ throwIfAborted(options.signal, 'Embedding request aborted.');
150
209
  if (provider === 'google' && process.env.GOOGLE_AI_KEY) {
151
- const vec = await getGeminiEmbedding(text);
210
+ const vec = await getGeminiEmbedding(text, options);
152
211
  if (vec) {
153
212
  return {
154
213
  vector: vec,
@@ -158,7 +217,7 @@ async function getEmbeddingWithMetadata(text, provider) {
158
217
  };
159
218
  }
160
219
  }
161
- const vec = await getOpenAIEmbedding(text);
220
+ const vec = await getOpenAIEmbedding(text, options);
162
221
  if (!vec) return null;
163
222
  return {
164
223
  vector: vec,
@@ -228,5 +287,7 @@ module.exports = {
228
287
  deserializeEmbedding,
229
288
  keywordSimilarity,
230
289
  EMBED_DIM,
231
- EMBED_DIM_GOOGLE
290
+ EMBED_DIM_GOOGLE,
291
+ GOOGLE_MODEL,
292
+ OPENAI_MODEL,
232
293
  };
@@ -4,6 +4,11 @@ const { v4: uuidv4 } = require('uuid');
4
4
  const db = require('../../db/database');
5
5
  const { resolveAgentId } = require('../agents/manager');
6
6
  const { getErrorMessage } = require('../bootstrap_helpers');
7
+ const {
8
+ createLinkedAbortController,
9
+ isAbortError,
10
+ throwIfAborted,
11
+ } = require('../../utils/abort');
7
12
  const { ingestDocuments } = require('./ingestion_documents');
8
13
  const {
9
14
  decorateProviderSnapshot,
@@ -39,6 +44,7 @@ class MemoryIngestionService {
39
44
  this.setInterval = setIntervalFn;
40
45
  this.clearInterval = clearIntervalFn;
41
46
  this.timer = null;
47
+ this.abortController = new AbortController();
42
48
  this.activeBatches = new Map();
43
49
  this.activeConnections = new Map();
44
50
  this.stopping = false;
@@ -67,6 +73,9 @@ class MemoryIngestionService {
67
73
  throw new Error('Memory ingestion cannot start while shutdown is in progress.');
68
74
  }
69
75
 
76
+ if (this.abortController.signal.aborted) {
77
+ this.abortController = new AbortController();
78
+ }
70
79
  this.stopping = false;
71
80
  this.state = 'running';
72
81
  this.lastError = null;
@@ -82,6 +91,7 @@ class MemoryIngestionService {
82
91
  try {
83
92
  await this.refreshDueConnections();
84
93
  } catch (err) {
94
+ if (isAbortError(err, this.abortController.signal) && this.stopping) return;
85
95
  this.lastError = getErrorMessage(err);
86
96
  console.warn('[MemoryIngestion] Background refresh failed:', this.lastError);
87
97
  }
@@ -91,6 +101,7 @@ class MemoryIngestionService {
91
101
  if (this.stopPromise) return this.stopPromise;
92
102
  this.stopping = true;
93
103
  this.state = 'stopping';
104
+ this.abortController.abort('Memory ingestion service is stopping.');
94
105
  if (this.timer) this.clearInterval(this.timer);
95
106
  this.timer = null;
96
107
 
@@ -111,13 +122,27 @@ class MemoryIngestionService {
111
122
  }
112
123
 
113
124
  async ingestDocuments(userId, documents = [], options = {}) {
114
- return ingestDocuments(this, userId, documents, options);
125
+ const linked = createLinkedAbortController([
126
+ this.abortController.signal,
127
+ options.signal,
128
+ ]);
129
+ try {
130
+ return await ingestDocuments(this, userId, documents, {
131
+ ...options,
132
+ signal: linked.signal,
133
+ });
134
+ } finally {
135
+ linked.cleanup();
136
+ }
115
137
  }
116
138
 
117
139
  refreshDueConnections(userId = null) {
118
140
  const scopeKey = userId == null ? 'all' : `user:${userId}`;
119
- if (this.stopping) {
120
- return Promise.resolve({ skipped: true, reason: 'service_stopping' });
141
+ if (this.stopping || this.abortController.signal.aborted) {
142
+ return Promise.resolve({
143
+ skipped: true,
144
+ reason: this.stopping ? 'service_stopping' : 'service_stopped',
145
+ });
121
146
  }
122
147
  const active = this.activeBatches.get(scopeKey);
123
148
  if (active) return active;
@@ -132,6 +157,7 @@ class MemoryIngestionService {
132
157
  }
133
158
 
134
159
  async _refreshDueConnections(userId = null) {
160
+ const signal = this.abortController.signal;
135
161
  this.lastRunAt = new Date().toISOString();
136
162
  try {
137
163
  const params = [];
@@ -145,8 +171,8 @@ class MemoryIngestionService {
145
171
  const connections = this.db.prepare(sql).all(...params);
146
172
  const results = [];
147
173
  for (const connection of connections) {
148
- if (this.stopping) break;
149
- results.push(await this._refreshConnectionSafely(connection));
174
+ if (this.stopping || signal.aborted) break;
175
+ results.push(await this._refreshConnectionSafely(connection, { signal }));
150
176
  }
151
177
  if (!results.some((result) => result?.status === 'failed')) {
152
178
  this.lastError = null;
@@ -154,19 +180,28 @@ class MemoryIngestionService {
154
180
  this.lastCompletedAt = new Date().toISOString();
155
181
  return { refreshed: results.length, results };
156
182
  } catch (err) {
183
+ if (isAbortError(err, signal) && this.stopping) {
184
+ return { refreshed: 0, results: [], cancelled: true };
185
+ }
157
186
  this.lastError = getErrorMessage(err);
158
187
  throw err;
159
188
  }
160
189
  }
161
190
 
162
- _refreshConnectionSafely(connection) {
191
+ _refreshConnectionSafely(connection, options = {}) {
163
192
  const connectionKey = `${connection.user_id}:${connection.id}`;
164
193
  const active = this.activeConnections.get(connectionKey);
165
194
  if (active) return active;
166
195
 
167
196
  const promise = Promise.resolve()
168
- .then(() => this.refreshConnection(connection))
197
+ .then(() => this.refreshConnection(connection, options))
169
198
  .catch((err) => {
199
+ if (isAbortError(err, options.signal) && this.stopping) {
200
+ return {
201
+ connectionId: connection.id,
202
+ status: 'cancelled',
203
+ };
204
+ }
170
205
  const error = getErrorMessage(err);
171
206
  this.lastError = error;
172
207
  console.warn(
@@ -214,7 +249,9 @@ class MemoryIngestionService {
214
249
  }
215
250
  }
216
251
 
217
- async refreshConnection(connection) {
252
+ async refreshConnection(connection, options = {}) {
253
+ const signal = options.signal || this.abortController.signal;
254
+ throwIfAborted(signal, 'Memory ingestion refresh aborted.');
218
255
  const sourceTypes = sourceTypesForConnection(connection.provider_key, connection.app_key);
219
256
  if (sourceTypes.length === 0) {
220
257
  return { connectionId: connection.id, status: 'not_supported' };
@@ -242,9 +279,12 @@ class MemoryIngestionService {
242
279
  connection,
243
280
  sourceTypes,
244
281
  cursor: latestJob?.cursor || {},
282
+ signal,
245
283
  });
246
284
  } catch (err) {
247
- this._recordCollectorFailure(connection, primarySource, policy, agentId, err);
285
+ if (!isAbortError(err, signal)) {
286
+ this._recordCollectorFailure(connection, primarySource, policy, agentId, err);
287
+ }
248
288
  throw err;
249
289
  }
250
290
  return this.ingestDocuments(connection.user_id, collected.documents || [], {
@@ -258,6 +298,7 @@ class MemoryIngestionService {
258
298
  sourceTypes,
259
299
  cursor: collected.cursor || null,
260
300
  },
301
+ signal,
261
302
  });
262
303
  }
263
304