neoagent 3.2.1-beta.2 → 3.2.1-beta.3
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/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +511 -89
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +29 -20
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +67 -13
- package/package.json +20 -13
- package/runtime/paths.js +49 -5
- package/server/guest_agent.js +52 -30
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +4 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +30803 -30805
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +5 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +44 -5
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +347 -162
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +12 -0
- package/server/services/ai/loop/conversation_loop.js +348 -242
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +8 -41
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +24 -19
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +67 -162
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +111 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +15 -28
- package/server/services/ai/providers/githubCopilot.js +36 -16
- package/server/services/ai/providers/google.js +23 -5
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +13 -22
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +102 -82
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/taskAnalysis.js +5 -1
- package/server/services/ai/terminal_reply.js +45 -0
- package/server/services/ai/toolEvidence.js +58 -29
- package/server/services/ai/tools.js +124 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +736 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +12 -1
- package/server/services/browser/extension/provider.js +59 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +158 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +87 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +84 -9
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +214 -66
- package/server/services/runtime/manager.js +17 -12
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- 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.
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
AND
|
|
115
|
-
AND
|
|
116
|
-
AND
|
|
117
|
-
AND
|
|
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 (
|
|
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 = '
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
timer = setTimeout(() => {
|
|
69
|
+
request?.destroy();
|
|
70
|
+
response?.destroy();
|
|
71
|
+
finish(null, null);
|
|
72
|
+
}, EMBEDDING_TIMEOUT_MS);
|
|
42
73
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
-
let
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
done(null);
|
|
135
|
+
request.on('error', (error) => {
|
|
136
|
+
if (isAbortError(error, signal)) finish(createAbortError(signal));
|
|
137
|
+
else finish(null, null);
|
|
73
138
|
});
|
|
74
|
-
|
|
75
|
-
req.end();
|
|
139
|
+
request.end(body);
|
|
76
140
|
});
|
|
77
141
|
}
|
|
78
142
|
|
|
79
|
-
|
|
80
|
-
|
|
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 =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
|