neoagent 3.2.1-beta.6 → 3.2.1-beta.8
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 +2 -2
- package/server/http/routes.js +7 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +5 -5
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/loop/completion_judge.js +16 -7
- package/server/services/ai/loop/conversation_loop.js +131 -122
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/models.js +1 -1
- package/server/services/ai/provider_selector.js +58 -3
- package/server/services/ai/providers/google.js +112 -92
- package/server/services/ai/terminal_reply.js +13 -1
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +7 -0
- package/server/services/runtime/guest_image.js +39 -9
- package/server/services/runtime/manager.js +35 -0
|
@@ -1,7 +1,63 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { GoogleGenAI } = require('@google/genai');
|
|
2
4
|
const { BaseProvider } = require('./base');
|
|
3
5
|
const { fetchResponseText } = require('../../network/http');
|
|
4
6
|
|
|
7
|
+
function parseToolArguments(value) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(value || '{}');
|
|
10
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
11
|
+
? parsed
|
|
12
|
+
: {};
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeUsage(usage) {
|
|
19
|
+
if (!usage) return null;
|
|
20
|
+
return {
|
|
21
|
+
inputTokens: usage.promptTokenCount || 0,
|
|
22
|
+
outputTokens: usage.candidatesTokenCount || 0,
|
|
23
|
+
reasoningTokens: usage.thoughtsTokenCount || 0,
|
|
24
|
+
cachedReadTokens: usage.cachedContentTokenCount || 0,
|
|
25
|
+
cacheWriteTokens: 0,
|
|
26
|
+
promptTokens: usage.promptTokenCount || 0,
|
|
27
|
+
completionTokens: usage.candidatesTokenCount || 0,
|
|
28
|
+
totalTokens: usage.totalTokenCount || 0
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function collectResponseParts(response, toolCalls, seenToolCalls = null) {
|
|
33
|
+
let content = '';
|
|
34
|
+
for (const candidate of response?.candidates || []) {
|
|
35
|
+
for (const part of candidate.content?.parts || []) {
|
|
36
|
+
if (part.text && part.thought !== true) content += part.text;
|
|
37
|
+
if (!part.functionCall?.name) continue;
|
|
38
|
+
|
|
39
|
+
const args = part.functionCall.args || {};
|
|
40
|
+
const providerCallId = String(part.functionCall.id || '').trim();
|
|
41
|
+
const signature = providerCallId
|
|
42
|
+
|| `${part.functionCall.name}:${JSON.stringify(args)}`;
|
|
43
|
+
if (seenToolCalls?.has(signature)) continue;
|
|
44
|
+
seenToolCalls?.add(signature);
|
|
45
|
+
|
|
46
|
+
toolCalls.push({
|
|
47
|
+
id: providerCallId
|
|
48
|
+
|| `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
49
|
+
type: 'function',
|
|
50
|
+
function: {
|
|
51
|
+
name: part.functionCall.name,
|
|
52
|
+
arguments: JSON.stringify(args),
|
|
53
|
+
thought_signature: part.thoughtSignature
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return content;
|
|
59
|
+
}
|
|
60
|
+
|
|
5
61
|
class GoogleProvider extends BaseProvider {
|
|
6
62
|
constructor(config = {}) {
|
|
7
63
|
super(config);
|
|
@@ -29,7 +85,7 @@ class GoogleProvider extends BaseProvider {
|
|
|
29
85
|
'gemini-1.5-flash': 1048576,
|
|
30
86
|
};
|
|
31
87
|
this.apiKey = config.apiKey || process.env.GOOGLE_AI_KEY;
|
|
32
|
-
this.genAI = new
|
|
88
|
+
this.genAI = new GoogleGenAI({ apiKey: this.apiKey });
|
|
33
89
|
}
|
|
34
90
|
|
|
35
91
|
async listModels(signal = null) {
|
|
@@ -78,11 +134,23 @@ class GoogleProvider extends BaseProvider {
|
|
|
78
134
|
functionDeclarations: tools.map(tool => ({
|
|
79
135
|
name: tool.name,
|
|
80
136
|
description: tool.description,
|
|
81
|
-
|
|
137
|
+
parametersJsonSchema: tool.parameters || { type: 'object', properties: {} }
|
|
82
138
|
}))
|
|
83
139
|
}];
|
|
84
140
|
}
|
|
85
141
|
|
|
142
|
+
buildGenerateConfig(systemInstruction, tools, options) {
|
|
143
|
+
const config = {};
|
|
144
|
+
if (systemInstruction) config.systemInstruction = systemInstruction;
|
|
145
|
+
if (tools.length > 0) config.tools = this.formatTools(tools);
|
|
146
|
+
if (options.signal) config.abortSignal = options.signal;
|
|
147
|
+
const maxOutputTokens = Number(options.maxTokens);
|
|
148
|
+
if (Number.isFinite(maxOutputTokens) && maxOutputTokens > 0) {
|
|
149
|
+
config.maxOutputTokens = Math.floor(maxOutputTokens);
|
|
150
|
+
}
|
|
151
|
+
return config;
|
|
152
|
+
}
|
|
153
|
+
|
|
86
154
|
convertMessages(messages) {
|
|
87
155
|
let systemInstruction = '';
|
|
88
156
|
const history = [];
|
|
@@ -93,14 +161,16 @@ class GoogleProvider extends BaseProvider {
|
|
|
93
161
|
continue;
|
|
94
162
|
}
|
|
95
163
|
if (msg.role === 'tool') {
|
|
164
|
+
const functionResponse = {
|
|
165
|
+
name: msg.name || 'tool',
|
|
166
|
+
response: { result: msg.content }
|
|
167
|
+
};
|
|
168
|
+
if (msg.tool_call_id) functionResponse.id = msg.tool_call_id;
|
|
96
169
|
history.push({
|
|
97
|
-
|
|
98
|
-
parts
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
response: { result: msg.content }
|
|
102
|
-
}
|
|
103
|
-
}]
|
|
170
|
+
// The current Gemini API represents tool results as functionResponse
|
|
171
|
+
// parts on a user turn.
|
|
172
|
+
role: 'user',
|
|
173
|
+
parts: [{ functionResponse }]
|
|
104
174
|
});
|
|
105
175
|
continue;
|
|
106
176
|
}
|
|
@@ -111,9 +181,10 @@ class GoogleProvider extends BaseProvider {
|
|
|
111
181
|
const functionCallPart = {
|
|
112
182
|
functionCall: {
|
|
113
183
|
name: tc.function.name,
|
|
114
|
-
args:
|
|
184
|
+
args: parseToolArguments(tc.function.arguments)
|
|
115
185
|
}
|
|
116
186
|
};
|
|
187
|
+
if (tc.id) functionCallPart.functionCall.id = tc.id;
|
|
117
188
|
if (tc.function.thought_signature) {
|
|
118
189
|
functionCallPart.thoughtSignature = tc.function.thought_signature;
|
|
119
190
|
}
|
|
@@ -147,6 +218,18 @@ class GoogleProvider extends BaseProvider {
|
|
|
147
218
|
normalizedHistory.push({ role: currentRole, parts: currentParts });
|
|
148
219
|
}
|
|
149
220
|
|
|
221
|
+
// Structured helpers can consist entirely of system instructions. Gemini
|
|
222
|
+
// still requires a user turn to trigger generation.
|
|
223
|
+
if (
|
|
224
|
+
normalizedHistory.length === 0
|
|
225
|
+
|| normalizedHistory[normalizedHistory.length - 1].role === 'model'
|
|
226
|
+
) {
|
|
227
|
+
normalizedHistory.push({
|
|
228
|
+
role: 'user',
|
|
229
|
+
parts: [{ text: 'Provide the requested response using the system instructions.' }]
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
150
233
|
if (normalizedHistory.length > 0 && normalizedHistory[0].role !== 'user') {
|
|
151
234
|
normalizedHistory.unshift({
|
|
152
235
|
role: 'user',
|
|
@@ -160,53 +243,18 @@ class GoogleProvider extends BaseProvider {
|
|
|
160
243
|
async chat(messages, tools = [], options = {}) {
|
|
161
244
|
const model = options.model || this.config.model || this.getDefaultModel();
|
|
162
245
|
const { systemInstruction, history } = this.convertMessages(messages);
|
|
163
|
-
|
|
164
|
-
const genModel = this.genAI.getGenerativeModel({
|
|
246
|
+
const response = await this.genAI.models.generateContent({
|
|
165
247
|
model,
|
|
166
|
-
|
|
167
|
-
|
|
248
|
+
contents: history,
|
|
249
|
+
config: this.buildGenerateConfig(systemInstruction, tools, options)
|
|
168
250
|
});
|
|
169
|
-
|
|
170
|
-
const lastMessage = history.pop();
|
|
171
|
-
const chat = genModel.startChat({ history });
|
|
172
|
-
const result = await chat.sendMessage(lastMessage.parts, { signal: options.signal });
|
|
173
|
-
const response = result.response;
|
|
174
|
-
|
|
175
|
-
let content = '';
|
|
176
251
|
const toolCalls = [];
|
|
177
|
-
|
|
178
|
-
for (const candidate of response.candidates || []) {
|
|
179
|
-
for (const part of candidate.content?.parts || []) {
|
|
180
|
-
if (part.text) content += part.text;
|
|
181
|
-
if (part.functionCall) {
|
|
182
|
-
toolCalls.push({
|
|
183
|
-
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
184
|
-
type: 'function',
|
|
185
|
-
function: {
|
|
186
|
-
name: part.functionCall.name,
|
|
187
|
-
arguments: JSON.stringify(part.functionCall.args || {}),
|
|
188
|
-
thought_signature: part.thoughtSignature
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const usage = response.usageMetadata;
|
|
252
|
+
const content = collectResponseParts(response, toolCalls);
|
|
196
253
|
return {
|
|
197
254
|
content,
|
|
198
255
|
toolCalls,
|
|
199
256
|
finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
|
|
200
|
-
usage:
|
|
201
|
-
inputTokens: usage.promptTokenCount || 0,
|
|
202
|
-
outputTokens: usage.candidatesTokenCount || 0,
|
|
203
|
-
reasoningTokens: usage.thoughtsTokenCount || 0,
|
|
204
|
-
cachedReadTokens: usage.cachedContentTokenCount || 0,
|
|
205
|
-
cacheWriteTokens: 0,
|
|
206
|
-
promptTokens: usage.promptTokenCount || 0,
|
|
207
|
-
completionTokens: usage.candidatesTokenCount || 0,
|
|
208
|
-
totalTokens: usage.totalTokenCount || 0
|
|
209
|
-
} : null,
|
|
257
|
+
usage: normalizeUsage(response.usageMetadata),
|
|
210
258
|
model
|
|
211
259
|
};
|
|
212
260
|
}
|
|
@@ -214,60 +262,32 @@ class GoogleProvider extends BaseProvider {
|
|
|
214
262
|
async *stream(messages, tools = [], options = {}) {
|
|
215
263
|
const model = options.model || this.config.model || this.getDefaultModel();
|
|
216
264
|
const { systemInstruction, history } = this.convertMessages(messages);
|
|
217
|
-
|
|
218
|
-
const genModel = this.genAI.getGenerativeModel({
|
|
265
|
+
const responseStream = await this.genAI.models.generateContentStream({
|
|
219
266
|
model,
|
|
220
|
-
|
|
221
|
-
|
|
267
|
+
contents: history,
|
|
268
|
+
config: this.buildGenerateConfig(systemInstruction, tools, options)
|
|
222
269
|
});
|
|
223
|
-
|
|
224
|
-
const lastMessage = history.pop();
|
|
225
|
-
const chat = genModel.startChat({ history });
|
|
226
|
-
const result = await chat.sendMessageStream(lastMessage.parts, { signal: options.signal });
|
|
227
|
-
|
|
228
270
|
let content = '';
|
|
229
271
|
const toolCalls = [];
|
|
272
|
+
const seenToolCalls = new Set();
|
|
273
|
+
let usage = null;
|
|
230
274
|
|
|
231
|
-
for await (const chunk of
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
yield { type: 'content', content: part.text };
|
|
237
|
-
}
|
|
238
|
-
if (part.functionCall) {
|
|
239
|
-
toolCalls.push({
|
|
240
|
-
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
241
|
-
type: 'function',
|
|
242
|
-
function: {
|
|
243
|
-
name: part.functionCall.name,
|
|
244
|
-
arguments: JSON.stringify(part.functionCall.args || {}),
|
|
245
|
-
thought_signature: part.thoughtSignature
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
275
|
+
for await (const chunk of responseStream) {
|
|
276
|
+
const chunkContent = collectResponseParts(chunk, toolCalls, seenToolCalls);
|
|
277
|
+
if (chunkContent) {
|
|
278
|
+
content += chunkContent;
|
|
279
|
+
yield { type: 'content', content: chunkContent };
|
|
250
280
|
}
|
|
281
|
+
if (chunk.usageMetadata) usage = chunk.usageMetadata;
|
|
251
282
|
}
|
|
252
283
|
|
|
253
|
-
const finalResponse = await result.response;
|
|
254
|
-
const usage = finalResponse.usageMetadata;
|
|
255
|
-
|
|
256
284
|
yield {
|
|
257
285
|
type: 'done',
|
|
258
286
|
content,
|
|
259
287
|
toolCalls,
|
|
260
288
|
finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
|
|
261
|
-
usage: usage
|
|
262
|
-
|
|
263
|
-
outputTokens: usage.candidatesTokenCount || 0,
|
|
264
|
-
reasoningTokens: usage.thoughtsTokenCount || 0,
|
|
265
|
-
cachedReadTokens: usage.cachedContentTokenCount || 0,
|
|
266
|
-
cacheWriteTokens: 0,
|
|
267
|
-
promptTokens: usage.promptTokenCount || 0,
|
|
268
|
-
completionTokens: usage.candidatesTokenCount || 0,
|
|
269
|
-
totalTokens: usage.totalTokenCount || 0
|
|
270
|
-
} : null
|
|
289
|
+
usage: normalizeUsage(usage),
|
|
290
|
+
model
|
|
271
291
|
};
|
|
272
292
|
}
|
|
273
293
|
}
|
|
@@ -10,7 +10,13 @@ function normalizeReply(content) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function hasExternalBlocker(text) {
|
|
13
|
-
return /\b(?:blocked|cannot|can't|could not|couldn't|unable to|do not have access|don't have access|missing (?:access|credentials|permission|information)|need you to|requires? your|waiting for your|please (?:provide|send|choose|confirm|authorize|approve))\b/.test(text);
|
|
13
|
+
return /\b(?:blocked|cannot|can't|could not|couldn't|unable to|do not have access|don't have access|missing (?:access|credentials|permission|information)|need you to|requires? your|waiting for your|please (?:provide|send|choose|confirm|authorize|approve)|blockiert|kann nicht|konnte nicht|mir fehlt|ich brauche von dir|warte auf dein(?:e|en)?|bitte (?:gib|schick|sende|nenn|bestätige|waehle|wähle|genehmige))\b/.test(text);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isTerminalQuestionOrBlockerReply(content) {
|
|
17
|
+
const text = normalizeReply(content);
|
|
18
|
+
if (!text) return false;
|
|
19
|
+
return /[??]/.test(text) || hasExternalBlocker(text);
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/**
|
|
@@ -36,10 +42,16 @@ function isDeferredWorkReply(content) {
|
|
|
36
42
|
/^(?:working on it|checking now|on it)[.!…]*$/,
|
|
37
43
|
/\bi(?:'ll| will)\s+(?:get back to you|update you|report back|let you know|keep you posted)\b/,
|
|
38
44
|
/\b(?:i(?:'ll| will)\s+follow up|stay tuned)\b/,
|
|
45
|
+
/^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?ich\s+(?:(?:arbeite|pruefe|prüfe|schaue|untersuche|teste|debugge|starte)\s+(?:(?:gerade|aktuell|jetzt|noch)\b|(?:das|dies|die logs?|den code|deine anfrage|deinen auftrag)\b)|(?:kuemmere|kümmere)\s+mich\s+(?:gerade|aktuell|jetzt|noch)\b)/,
|
|
46
|
+
/^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?ich\s+(?:werde|wuerde|würde)\s+(?:jetzt\s+)?(?:pruefen|prüfen|nachsehen|untersuchen|testen|debuggen|starten|fixen|erledigen)\b/,
|
|
47
|
+
/^(?:(?:klar|okay|ok|alles klar)[,.!]?\s+)?(?:lass|lasst)\s+mich\s+(?:(?:das|dies|die logs?|den code)\s+)?(?:pruefen|prüfen|nachsehen|untersuchen|testen|debuggen)\b/,
|
|
48
|
+
/^(?:gib mir|gebt mir)\s+(?:einen\s+)?(?:moment|augenblick)|^(?:bin dran|mache ich)[.!…]*$/,
|
|
49
|
+
/\bich\s+(?:melde mich|gebe dir bescheid|halte dich auf dem laufenden)\b/,
|
|
39
50
|
];
|
|
40
51
|
return patterns.some((pattern) => pattern.test(text));
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
module.exports = {
|
|
44
55
|
isDeferredWorkReply,
|
|
56
|
+
isTerminalQuestionOrBlockerReply,
|
|
45
57
|
};
|
|
@@ -306,6 +306,15 @@ class DockerVMManager {
|
|
|
306
306
|
return Boolean(session && isContainerRunning(session.containerId));
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// A side-effect-free snapshot for request-path capability reporting. Unlike
|
|
310
|
+
// hasVm(), this deliberately avoids a synchronous `docker inspect`, which can
|
|
311
|
+
// block the Node event loop. Tool execution still calls ensureVm() and performs
|
|
312
|
+
// the authoritative liveness check before using a tracked session.
|
|
313
|
+
hasTrackedVm(userId) {
|
|
314
|
+
const key = String(userId || '').trim();
|
|
315
|
+
return Boolean(key && this.instances.has(key));
|
|
316
|
+
}
|
|
317
|
+
|
|
309
318
|
// Used by validation.js — cached to avoid docker calls on every status poll.
|
|
310
319
|
getReadiness() {
|
|
311
320
|
const now = Date.now();
|
|
@@ -13,6 +13,8 @@ const GUEST_PAYLOAD_PROFILES = Object.freeze({
|
|
|
13
13
|
{ source: 'runtime/paths.js', target: 'runtime/paths.js' },
|
|
14
14
|
{ source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
|
|
15
15
|
{ source: 'server/services/browser', target: 'server/services/browser' },
|
|
16
|
+
{ source: 'server/services/android/process.js', target: 'server/services/android/process.js' },
|
|
17
|
+
{ source: 'server/utils/cloud-security.js', target: 'server/utils/cloud-security.js' },
|
|
16
18
|
],
|
|
17
19
|
cli: [
|
|
18
20
|
{ source: 'server/guest-agent.cli.package.json', target: 'package.json' },
|
|
@@ -28,6 +30,8 @@ const GUEST_PAYLOAD_PROFILES = Object.freeze({
|
|
|
28
30
|
{ source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
|
|
29
31
|
{ source: 'server/services/cli', target: 'server/services/cli' },
|
|
30
32
|
{ source: 'server/services/browser', target: 'server/services/browser' },
|
|
33
|
+
{ source: 'server/services/android/process.js', target: 'server/services/android/process.js' },
|
|
34
|
+
{ source: 'server/utils/cloud-security.js', target: 'server/utils/cloud-security.js' },
|
|
31
35
|
],
|
|
32
36
|
android: [
|
|
33
37
|
{ source: 'server/guest-agent.android.package.json', target: 'package.json' },
|
|
@@ -36,6 +40,9 @@ const GUEST_PAYLOAD_PROFILES = Object.freeze({
|
|
|
36
40
|
{ source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
|
|
37
41
|
{ source: 'server/services/cli', target: 'server/services/cli' },
|
|
38
42
|
{ source: 'server/services/android', target: 'server/services/android' },
|
|
43
|
+
{ source: 'server/utils/abort.js', target: 'server/utils/abort.js' },
|
|
44
|
+
{ source: 'server/utils/cloud-security.js', target: 'server/utils/cloud-security.js' },
|
|
45
|
+
{ source: 'server/utils/image_payload.js', target: 'server/utils/image_payload.js' },
|
|
39
46
|
],
|
|
40
47
|
});
|
|
41
48
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
-
const { spawnSync } = require('child_process');
|
|
6
|
+
const { spawn, spawnSync } = require('child_process');
|
|
7
7
|
const { DATA_DIR } = require('../../../runtime/paths');
|
|
8
8
|
const { stageGuestPayload, normalizeRuntimeProfile } = require('./guest_bootstrap');
|
|
9
9
|
|
|
@@ -105,6 +105,34 @@ function dockerAvailable() {
|
|
|
105
105
|
return !result.error && result.status === 0;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function runDockerCommand(args, options = {}) {
|
|
109
|
+
const timeoutMs = Number(options.timeout || BUILD_TIMEOUT_MS);
|
|
110
|
+
const spawnImpl = options.spawnImpl || spawn;
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const child = spawnImpl('docker', args, {
|
|
113
|
+
stdio: options.stdio || ['ignore', 'inherit', 'inherit'],
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
});
|
|
116
|
+
let settled = false;
|
|
117
|
+
const finish = (error, status = null) => {
|
|
118
|
+
if (settled) return;
|
|
119
|
+
settled = true;
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
if (error) reject(error);
|
|
122
|
+
else resolve(status);
|
|
123
|
+
};
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
const error = new Error(`docker ${args[0]} timed out after ${timeoutMs}ms`);
|
|
126
|
+
error.code = 'DOCKER_COMMAND_TIMEOUT';
|
|
127
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
128
|
+
finish(error);
|
|
129
|
+
}, timeoutMs);
|
|
130
|
+
timeout.unref?.();
|
|
131
|
+
child.once('error', (error) => finish(error));
|
|
132
|
+
child.once('close', (status) => finish(null, status));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
108
136
|
// Builds and caches the per-profile guest-agent Docker image. The image bakes in
|
|
109
137
|
// the guest agent, its dependencies, and (for browser_cli) the Chromium browser,
|
|
110
138
|
// so containers start the agent directly with no runtime installation step.
|
|
@@ -166,15 +194,16 @@ class GuestImageBuilder {
|
|
|
166
194
|
async #build(tag) {
|
|
167
195
|
console.log(`[GuestImage:${this.profile}] Building guest image ${tag} (one-time; downloads browser + deps)…`);
|
|
168
196
|
const started = Date.now();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
197
|
+
let status;
|
|
198
|
+
try {
|
|
199
|
+
status = await runDockerCommand(['build', '-t', tag, this.contextDir], {
|
|
200
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error(`Guest image build failed: ${error.message}`, { cause: error });
|
|
175
204
|
}
|
|
176
|
-
if (
|
|
177
|
-
throw new Error(`Guest image build for ${tag} exited with status ${
|
|
205
|
+
if (status !== 0) {
|
|
206
|
+
throw new Error(`Guest image build for ${tag} exited with status ${status}`);
|
|
178
207
|
}
|
|
179
208
|
this.#cachedBuiltAt = Date.now();
|
|
180
209
|
console.log(`[GuestImage:${this.profile}] Built ${tag} in ${Math.round((Date.now() - started) / 1000)}s`);
|
|
@@ -185,5 +214,6 @@ class GuestImageBuilder {
|
|
|
185
214
|
module.exports = {
|
|
186
215
|
GuestImageBuilder,
|
|
187
216
|
dockerAvailable,
|
|
217
|
+
runDockerCommand,
|
|
188
218
|
BASE_IMAGE,
|
|
189
219
|
};
|
|
@@ -87,6 +87,41 @@ class RuntimeManager {
|
|
|
87
87
|
);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
getCapabilitySnapshot(userId) {
|
|
91
|
+
const settings = this.getSettings(userId) || {};
|
|
92
|
+
const key = String(userId || '').trim();
|
|
93
|
+
const extensionConnected = this.hasActiveExtensionBrowser(userId);
|
|
94
|
+
const androidController = key && this.androidControllers instanceof Map
|
|
95
|
+
? this.androidControllers.get(key)
|
|
96
|
+
: null;
|
|
97
|
+
let androidStatus = null;
|
|
98
|
+
if (androidController && typeof androidController.getStatusSync === 'function') {
|
|
99
|
+
try {
|
|
100
|
+
androidStatus = androidController.getStatusSync();
|
|
101
|
+
} catch {
|
|
102
|
+
androidStatus = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
browser: {
|
|
108
|
+
preferredBackend: settings.browser_backend,
|
|
109
|
+
activeBackend: extensionConnected ? 'extension' : 'vm',
|
|
110
|
+
extensionConnected,
|
|
111
|
+
vmInitialized: Boolean(
|
|
112
|
+
this.browserBackend?.vmManager?.hasTrackedVm?.(userId),
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
android: {
|
|
116
|
+
initialized: Boolean(androidController),
|
|
117
|
+
status: androidStatus,
|
|
118
|
+
},
|
|
119
|
+
desktop: {
|
|
120
|
+
connected: this.hasActiveDesktopCompanion(userId),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
90
125
|
resolveBackend(userId, requested) {
|
|
91
126
|
void userId;
|
|
92
127
|
return requested === 'browser' ? this.browserBackend : this.cliBackend;
|