neoagent 2.4.1-beta.9 → 2.4.1
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/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +36 -4
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -3
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +13 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +2 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +197 -42
- package/flutter_app/lib/main_devices.dart +436 -4
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +673 -871
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +46 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +42 -3
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88537 -85936
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +10 -2
- package/server/routes/stream.js +1 -1
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +78 -13
- package/server/services/browser/extension/registry.js +94 -9
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -53
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/streaming/android-stream.js +293 -40
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -274,6 +274,42 @@ function markProactiveMessageSent({ runState, deliveryState, content }) {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
function markProactiveNoResponse({ runState, deliveryState }) {
|
|
278
|
+
if (runState) {
|
|
279
|
+
runState.noResponse = true;
|
|
280
|
+
}
|
|
281
|
+
if (deliveryState) {
|
|
282
|
+
deliveryState.noResponse = true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeStoredSettingString(value) {
|
|
287
|
+
if (value == null) return '';
|
|
288
|
+
if (typeof value !== 'string') return String(value || '').trim();
|
|
289
|
+
let current = value.trim();
|
|
290
|
+
for (let i = 0; i < 2; i += 1) {
|
|
291
|
+
if (!current) return '';
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(current);
|
|
294
|
+
if (typeof parsed === 'string') {
|
|
295
|
+
current = parsed.trim();
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
return '';
|
|
299
|
+
} catch {
|
|
300
|
+
return current;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return current;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function normalizeMessagingTarget(target = {}) {
|
|
307
|
+
const platform = normalizeStoredSettingString(target.platform);
|
|
308
|
+
const to = normalizeStoredSettingString(target.to);
|
|
309
|
+
if (!platform || !to) return null;
|
|
310
|
+
return { platform, to };
|
|
311
|
+
}
|
|
312
|
+
|
|
277
313
|
function buildAndroidUiMatchProperties(extra = {}) {
|
|
278
314
|
return {
|
|
279
315
|
x: { type: 'number', description: 'Absolute X coordinate' },
|
|
@@ -1289,7 +1325,7 @@ function getAvailableTools(app, options = {}) {
|
|
|
1289
1325
|
// mechanism and gives the AI real agency over when it's finished.
|
|
1290
1326
|
tools.push({
|
|
1291
1327
|
name: 'task_complete',
|
|
1292
|
-
description: 'Signal that the task is fully complete and provide the final response. Call this exactly once when all steps are done and you have a complete answer ready. Do NOT call it if you still have work to do.',
|
|
1328
|
+
description: 'Signal that the task is fully complete and provide the final response. Call this exactly once when all steps are done and you have a complete answer ready. Do NOT call it if you still have work to do, unverified claims, unresolved tool failures, or confidence below the current run requirement.',
|
|
1293
1329
|
parameters: {
|
|
1294
1330
|
type: 'object',
|
|
1295
1331
|
properties: {
|
|
@@ -1300,10 +1336,10 @@ function getAvailableTools(app, options = {}) {
|
|
|
1300
1336
|
confidence: {
|
|
1301
1337
|
type: 'string',
|
|
1302
1338
|
enum: ['high', 'medium', 'low'],
|
|
1303
|
-
description: 'How confident are you the task is fully and correctly complete? Use "low"
|
|
1339
|
+
description: 'How confident are you the task is fully and correctly complete? Use "low" only when the final answer is intentionally limited or incomplete; low confidence may be rejected so the run can keep working.'
|
|
1304
1340
|
}
|
|
1305
1341
|
},
|
|
1306
|
-
required: ['message']
|
|
1342
|
+
required: ['message', 'confidence']
|
|
1307
1343
|
}
|
|
1308
1344
|
});
|
|
1309
1345
|
|
|
@@ -2112,6 +2148,9 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2112
2148
|
error: proactiveValidation.error,
|
|
2113
2149
|
};
|
|
2114
2150
|
}
|
|
2151
|
+
if (proactiveValidation.reason === 'no_response') {
|
|
2152
|
+
markProactiveNoResponse({ runState, deliveryState });
|
|
2153
|
+
}
|
|
2115
2154
|
return {
|
|
2116
2155
|
sent: false,
|
|
2117
2156
|
suppressed: proactiveValidation.suppressed === true,
|
|
@@ -2323,8 +2362,8 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2323
2362
|
|| null
|
|
2324
2363
|
);
|
|
2325
2364
|
const loadDefaultTarget = () => ({
|
|
2326
|
-
platform: loadAgentSetting('last_platform'),
|
|
2327
|
-
to: loadAgentSetting('last_chat_id')
|
|
2365
|
+
platform: normalizeStoredSettingString(loadAgentSetting('last_platform')),
|
|
2366
|
+
to: normalizeStoredSettingString(loadAgentSetting('last_chat_id'))
|
|
2328
2367
|
});
|
|
2329
2368
|
|
|
2330
2369
|
let taskConfig = null;
|
|
@@ -2336,26 +2375,43 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2336
2375
|
try {
|
|
2337
2376
|
taskConfig = JSON.parse(task.task_config || '{}');
|
|
2338
2377
|
taskTarget = {
|
|
2339
|
-
platform: taskConfig.notifyPlatform
|
|
2340
|
-
to: taskConfig.notifyTo
|
|
2378
|
+
platform: normalizeStoredSettingString(taskConfig.notifyPlatform),
|
|
2379
|
+
to: normalizeStoredSettingString(taskConfig.notifyTo)
|
|
2341
2380
|
};
|
|
2342
2381
|
} catch { }
|
|
2343
2382
|
}
|
|
2344
2383
|
}
|
|
2345
2384
|
|
|
2346
2385
|
const fallbackTarget = loadDefaultTarget();
|
|
2386
|
+
const recentTargets = db.prepare(
|
|
2387
|
+
`SELECT platform, platform_chat_id
|
|
2388
|
+
FROM messages
|
|
2389
|
+
WHERE user_id = ?
|
|
2390
|
+
AND agent_id = ?
|
|
2391
|
+
AND platform IS NOT NULL
|
|
2392
|
+
AND platform_chat_id IS NOT NULL
|
|
2393
|
+
ORDER BY id DESC
|
|
2394
|
+
LIMIT 20`
|
|
2395
|
+
).all(userId, agentId);
|
|
2347
2396
|
const candidateTargets = [];
|
|
2348
2397
|
const seenTargets = new Set();
|
|
2349
2398
|
const addCandidate = (target) => {
|
|
2350
|
-
|
|
2351
|
-
|
|
2399
|
+
const normalizedTarget = normalizeMessagingTarget(target);
|
|
2400
|
+
if (!normalizedTarget) return;
|
|
2401
|
+
const key = `${normalizedTarget.platform}:${normalizedTarget.to}`;
|
|
2352
2402
|
if (seenTargets.has(key)) return;
|
|
2353
2403
|
seenTargets.add(key);
|
|
2354
|
-
candidateTargets.push(
|
|
2404
|
+
candidateTargets.push(normalizedTarget);
|
|
2355
2405
|
};
|
|
2356
2406
|
|
|
2357
2407
|
addCandidate(taskTarget);
|
|
2358
2408
|
addCandidate(fallbackTarget);
|
|
2409
|
+
for (const row of recentTargets) {
|
|
2410
|
+
addCandidate({
|
|
2411
|
+
platform: row.platform,
|
|
2412
|
+
to: row.platform_chat_id
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2359
2415
|
|
|
2360
2416
|
if (candidateTargets.length === 0) {
|
|
2361
2417
|
throw new Error('No messaging target is configured for this task run. Connect a platform and send at least one message on this server, or recreate the task after reconnecting.');
|
|
@@ -2611,8 +2667,14 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2611
2667
|
let tools = [];
|
|
2612
2668
|
if (autoStart) {
|
|
2613
2669
|
try {
|
|
2614
|
-
await mcpClient.startServer(
|
|
2615
|
-
|
|
2670
|
+
const startResult = await mcpClient.startServer(
|
|
2671
|
+
serverId,
|
|
2672
|
+
args.command,
|
|
2673
|
+
args.name,
|
|
2674
|
+
userId,
|
|
2675
|
+
{ agentId }
|
|
2676
|
+
);
|
|
2677
|
+
tools = startResult.tools || [];
|
|
2616
2678
|
} catch (startErr) {
|
|
2617
2679
|
return { registered: true, id: serverId, started: false, error: `Registered but failed to start: ${startErr.message}` };
|
|
2618
2680
|
}
|
|
@@ -2635,7 +2697,10 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2635
2697
|
args: JSON.parse(s.config || '{}').args || [],
|
|
2636
2698
|
enabled: !!s.enabled,
|
|
2637
2699
|
status: liveStatuses[s.id]?.status || 'stopped',
|
|
2638
|
-
toolCount: liveStatuses[s.id]?.toolCount || 0
|
|
2700
|
+
toolCount: liveStatuses[s.id]?.toolCount || 0,
|
|
2701
|
+
error: liveStatuses[s.id]?.error || null,
|
|
2702
|
+
consecutiveFails: liveStatuses[s.id]?.consecutiveFails || 0,
|
|
2703
|
+
nextRetryAt: liveStatuses[s.id]?.nextRetryAt || null
|
|
2639
2704
|
}))
|
|
2640
2705
|
};
|
|
2641
2706
|
}
|
|
@@ -10,6 +10,7 @@ const DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1000;
|
|
|
10
10
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 1000;
|
|
11
11
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 25 * 1000;
|
|
12
12
|
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 75 * 1000;
|
|
13
|
+
const DEFAULT_PRESENCE_TOUCH_INTERVAL_MS = 15 * 1000;
|
|
13
14
|
|
|
14
15
|
function sha256(value) {
|
|
15
16
|
return crypto.createHash('sha256').update(String(value || '')).digest('hex');
|
|
@@ -46,6 +47,7 @@ class BrowserExtensionRegistry {
|
|
|
46
47
|
this.pairingTtlMs = Number(options.pairingTtlMs || process.env.NEOAGENT_BROWSER_EXTENSION_PAIRING_TTL_MS || DEFAULT_PAIRING_TTL_MS);
|
|
47
48
|
this.heartbeatIntervalMs = Number(options.heartbeatIntervalMs || process.env.NEOAGENT_BROWSER_EXTENSION_HEARTBEAT_INTERVAL_MS || DEFAULT_HEARTBEAT_INTERVAL_MS);
|
|
48
49
|
this.heartbeatTimeoutMs = Number(options.heartbeatTimeoutMs || process.env.NEOAGENT_BROWSER_EXTENSION_HEARTBEAT_TIMEOUT_MS || DEFAULT_HEARTBEAT_TIMEOUT_MS);
|
|
50
|
+
this.presenceTouchIntervalMs = Number(options.presenceTouchIntervalMs || process.env.NEOAGENT_BROWSER_EXTENSION_PRESENCE_TOUCH_INTERVAL_MS || DEFAULT_PRESENCE_TOUCH_INTERVAL_MS);
|
|
49
51
|
this.connectionsByUser = new Map();
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -142,16 +144,42 @@ class BrowserExtensionRegistry {
|
|
|
142
144
|
};
|
|
143
145
|
|
|
144
146
|
const tx = this.db.transaction(() => {
|
|
147
|
+
// Find other active tokens with the same name for this user.
|
|
148
|
+
// If they are currently offline, revoke them to clean up old installs/duplicates.
|
|
149
|
+
const duplicates = this.db.prepare(
|
|
150
|
+
`SELECT id FROM browser_extension_tokens
|
|
151
|
+
WHERE user_id = ? AND name = ? AND status = 'active'`
|
|
152
|
+
).all(row.user_id, extensionName);
|
|
153
|
+
|
|
154
|
+
for (const dup of duplicates) {
|
|
155
|
+
if (!this.isConnected(row.user_id, dup.id)) {
|
|
156
|
+
this.db.prepare(
|
|
157
|
+
`UPDATE browser_extension_tokens
|
|
158
|
+
SET status = 'revoked', revoked_at = datetime('now')
|
|
159
|
+
WHERE id = ?`
|
|
160
|
+
).run(dup.id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
145
164
|
this.db.prepare(
|
|
146
165
|
`UPDATE browser_extension_pairing_requests
|
|
147
166
|
SET status = 'claimed', claimed_at = datetime('now')
|
|
148
167
|
WHERE id = ?`
|
|
149
168
|
).run(row.id);
|
|
169
|
+
|
|
150
170
|
this.db.prepare(
|
|
151
171
|
`INSERT INTO browser_extension_tokens (
|
|
152
172
|
id, user_id, token_hash, name, status, metadata_json
|
|
153
173
|
) VALUES (?, ?, ?, ?, 'active', ?)`
|
|
154
174
|
).run(tokenId, row.user_id, sha256(token), extensionName, safeJson(metadata));
|
|
175
|
+
|
|
176
|
+
// Auto-select the newly paired token on claim
|
|
177
|
+
const write = this.db.prepare(
|
|
178
|
+
`INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?)
|
|
179
|
+
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value`
|
|
180
|
+
);
|
|
181
|
+
write.run(row.user_id, 'browser_extension_token_id', tokenId);
|
|
182
|
+
write.run(row.user_id, 'selected_browser_extension_token_id', tokenId);
|
|
155
183
|
});
|
|
156
184
|
tx();
|
|
157
185
|
|
|
@@ -192,6 +220,7 @@ class BrowserExtensionRegistry {
|
|
|
192
220
|
timeoutMs: this.commandTimeoutMs,
|
|
193
221
|
heartbeatIntervalMs: this.heartbeatIntervalMs,
|
|
194
222
|
heartbeatTimeoutMs: this.heartbeatTimeoutMs,
|
|
223
|
+
presenceTouchIntervalMs: this.presenceTouchIntervalMs,
|
|
195
224
|
});
|
|
196
225
|
userMap.set(tokenRow.id, connection);
|
|
197
226
|
this.db.prepare(
|
|
@@ -260,17 +289,44 @@ class BrowserExtensionRegistry {
|
|
|
260
289
|
if (selected && userMap.get(selected)?.isOpen()) {
|
|
261
290
|
return userMap.get(selected);
|
|
262
291
|
}
|
|
263
|
-
|
|
292
|
+
|
|
293
|
+
// Auto-select online connection if selected is offline/unset
|
|
264
294
|
const online = Array.from(userMap.values()).filter((connection) => connection.isOpen());
|
|
265
|
-
if (online.length
|
|
266
|
-
|
|
267
|
-
|
|
295
|
+
if (online.length > 0) {
|
|
296
|
+
online.sort((a, b) => String(b.connectedAt || '').localeCompare(String(a.connectedAt || '')));
|
|
297
|
+
const activeConnection = online[0];
|
|
298
|
+
try {
|
|
299
|
+
const write = this.db.prepare(
|
|
300
|
+
`INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?)
|
|
301
|
+
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value`
|
|
302
|
+
);
|
|
303
|
+
write.run(userId, 'browser_extension_token_id', activeConnection.tokenId);
|
|
304
|
+
write.run(userId, 'selected_browser_extension_token_id', activeConnection.tokenId);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.error('[Registry] failed to auto-select online extension', err);
|
|
307
|
+
}
|
|
308
|
+
return activeConnection;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (selected) {
|
|
312
|
+
return userMap.get(selected) || null;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
268
315
|
}
|
|
269
316
|
|
|
270
317
|
isConnected(userId, tokenId = null) {
|
|
271
318
|
return Boolean(this.getConnection(userId, tokenId)?.isOpen());
|
|
272
319
|
}
|
|
273
320
|
|
|
321
|
+
touchPresence(userId, tokenId) {
|
|
322
|
+
const userMap = this.#getUserConnections(userId);
|
|
323
|
+
const connection = userMap?.get(String(tokenId));
|
|
324
|
+
if (!connection?.isOpen()) return;
|
|
325
|
+
this.db.prepare(
|
|
326
|
+
`UPDATE browser_extension_tokens SET last_seen_at = datetime('now') WHERE id = ?`
|
|
327
|
+
).run(tokenId);
|
|
328
|
+
}
|
|
329
|
+
|
|
274
330
|
async dispatch(userId, command, payload = {}, options = {}) {
|
|
275
331
|
const connection = this.getConnection(userId, options.tokenId || payload.tokenId || null);
|
|
276
332
|
if (!connection || !connection.isOpen()) {
|
|
@@ -285,10 +341,16 @@ class BrowserExtensionRegistry {
|
|
|
285
341
|
|
|
286
342
|
getStatus(userId) {
|
|
287
343
|
const userMap = this.#getUserConnections(userId);
|
|
288
|
-
|
|
289
|
-
|
|
344
|
+
let selectedTokenId = this.getSelectedTokenId(userId);
|
|
345
|
+
let connected = selectedTokenId
|
|
290
346
|
? userMap?.get(selectedTokenId)
|
|
291
|
-
:
|
|
347
|
+
: null;
|
|
348
|
+
if (!connected?.isOpen()) {
|
|
349
|
+
connected = this.getConnection(userId);
|
|
350
|
+
if (connected?.isOpen()) {
|
|
351
|
+
selectedTokenId = connected.tokenId;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
292
354
|
const effectiveSelectedTokenId = selectedTokenId || connected?.tokenId || null;
|
|
293
355
|
const tokens = this.db.prepare(
|
|
294
356
|
`SELECT id, name, status, last_connected_at, last_seen_at, revoked_at, created_at, metadata_json
|
|
@@ -356,7 +418,7 @@ class BrowserExtensionRegistry {
|
|
|
356
418
|
}
|
|
357
419
|
|
|
358
420
|
class ExtensionBrowserConnection {
|
|
359
|
-
constructor({ registry, ws, userId, tokenId, meta, timeoutMs, heartbeatIntervalMs, heartbeatTimeoutMs }) {
|
|
421
|
+
constructor({ registry, ws, userId, tokenId, meta, timeoutMs, heartbeatIntervalMs, heartbeatTimeoutMs, presenceTouchIntervalMs }) {
|
|
360
422
|
this.registry = registry;
|
|
361
423
|
this.ws = ws;
|
|
362
424
|
this.userId = userId;
|
|
@@ -365,13 +427,18 @@ class ExtensionBrowserConnection {
|
|
|
365
427
|
this.timeoutMs = timeoutMs;
|
|
366
428
|
this.heartbeatIntervalMs = heartbeatIntervalMs;
|
|
367
429
|
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
|
|
430
|
+
this.presenceTouchIntervalMs = presenceTouchIntervalMs;
|
|
368
431
|
this.pending = new Map();
|
|
369
432
|
this.connectedAt = new Date().toISOString();
|
|
370
433
|
this.lastPongAt = Date.now();
|
|
434
|
+
this.lastPresenceTouchAt = 0;
|
|
371
435
|
this.heartbeatTimer = null;
|
|
372
436
|
|
|
373
437
|
ws.on('message', (data) => this.#handleMessage(data));
|
|
374
|
-
ws.on('pong', () => {
|
|
438
|
+
ws.on('pong', () => {
|
|
439
|
+
this.lastPongAt = Date.now();
|
|
440
|
+
this.touchPresence();
|
|
441
|
+
});
|
|
375
442
|
ws.on('close', () => this.#closePending(new ExtensionBrowserUnavailableError('Extension browser disconnected.')));
|
|
376
443
|
ws.on('error', (error) => this.#closePending(error));
|
|
377
444
|
this.#startHeartbeat();
|
|
@@ -391,6 +458,21 @@ class ExtensionBrowserConnection {
|
|
|
391
458
|
this.#closePending(new ExtensionBrowserUnavailableError('Extension browser disconnected.'));
|
|
392
459
|
}
|
|
393
460
|
|
|
461
|
+
touchPresence({ force = false } = {}) {
|
|
462
|
+
const now = Date.now();
|
|
463
|
+
const intervalMs = Number(this.presenceTouchIntervalMs);
|
|
464
|
+
if (
|
|
465
|
+
!force
|
|
466
|
+
&& Number.isFinite(intervalMs)
|
|
467
|
+
&& intervalMs > 0
|
|
468
|
+
&& now - this.lastPresenceTouchAt < intervalMs
|
|
469
|
+
) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this.lastPresenceTouchAt = now;
|
|
473
|
+
this.registry.touchPresence(this.userId, this.tokenId);
|
|
474
|
+
}
|
|
475
|
+
|
|
394
476
|
sendCommand(command, payload = {}, options = {}) {
|
|
395
477
|
if (!this.isOpen()) {
|
|
396
478
|
return Promise.reject(new ExtensionBrowserUnavailableError());
|
|
@@ -416,6 +498,7 @@ class ExtensionBrowserConnection {
|
|
|
416
498
|
|
|
417
499
|
#handleMessage(data) {
|
|
418
500
|
this.lastPongAt = Date.now();
|
|
501
|
+
this.touchPresence();
|
|
419
502
|
let message;
|
|
420
503
|
try {
|
|
421
504
|
message = parseExtensionMessage(data);
|
|
@@ -453,6 +536,7 @@ class ExtensionBrowserConnection {
|
|
|
453
536
|
const intervalMs = Number(this.heartbeatIntervalMs);
|
|
454
537
|
const timeoutMs = Number(this.heartbeatTimeoutMs);
|
|
455
538
|
if (!Number.isFinite(intervalMs) || intervalMs <= 0) return;
|
|
539
|
+
this.touchPresence({ force: true });
|
|
456
540
|
this.heartbeatTimer = setInterval(() => {
|
|
457
541
|
if (!this.isOpen()) {
|
|
458
542
|
this.#closePending(new ExtensionBrowserUnavailableError('Extension browser disconnected.'));
|
|
@@ -463,6 +547,7 @@ class ExtensionBrowserConnection {
|
|
|
463
547
|
this.#closePending(new ExtensionBrowserUnavailableError('Extension browser heartbeat timed out.'));
|
|
464
548
|
return;
|
|
465
549
|
}
|
|
550
|
+
this.touchPresence();
|
|
466
551
|
try {
|
|
467
552
|
this.ws.ping();
|
|
468
553
|
} catch (error) {
|
|
@@ -10,6 +10,9 @@ const {
|
|
|
10
10
|
} = require('./protocol');
|
|
11
11
|
|
|
12
12
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 1000;
|
|
13
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 25 * 1000;
|
|
14
|
+
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 75 * 1000;
|
|
15
|
+
const DEFAULT_PRESENCE_TOUCH_INTERVAL_MS = 15 * 1000;
|
|
13
16
|
|
|
14
17
|
function safeJson(value) {
|
|
15
18
|
try {
|
|
@@ -47,6 +50,21 @@ class DesktopCompanionRegistry {
|
|
|
47
50
|
|| process.env.NEOAGENT_DESKTOP_COMMAND_TIMEOUT_MS
|
|
48
51
|
|| DEFAULT_COMMAND_TIMEOUT_MS,
|
|
49
52
|
);
|
|
53
|
+
this.heartbeatIntervalMs = Number(
|
|
54
|
+
options.heartbeatIntervalMs
|
|
55
|
+
|| process.env.NEOAGENT_DESKTOP_HEARTBEAT_INTERVAL_MS
|
|
56
|
+
|| DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
57
|
+
);
|
|
58
|
+
this.heartbeatTimeoutMs = Number(
|
|
59
|
+
options.heartbeatTimeoutMs
|
|
60
|
+
|| process.env.NEOAGENT_DESKTOP_HEARTBEAT_TIMEOUT_MS
|
|
61
|
+
|| DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
62
|
+
);
|
|
63
|
+
this.presenceTouchIntervalMs = Number(
|
|
64
|
+
options.presenceTouchIntervalMs
|
|
65
|
+
|| process.env.NEOAGENT_DESKTOP_PRESENCE_TOUCH_INTERVAL_MS
|
|
66
|
+
|| DEFAULT_PRESENCE_TOUCH_INTERVAL_MS,
|
|
67
|
+
);
|
|
50
68
|
this.connectionsByUser = new Map();
|
|
51
69
|
}
|
|
52
70
|
|
|
@@ -200,6 +218,9 @@ class DesktopCompanionRegistry {
|
|
|
200
218
|
platform: record.platform,
|
|
201
219
|
},
|
|
202
220
|
timeoutMs: this.commandTimeoutMs,
|
|
221
|
+
heartbeatIntervalMs: this.heartbeatIntervalMs,
|
|
222
|
+
heartbeatTimeoutMs: this.heartbeatTimeoutMs,
|
|
223
|
+
presenceTouchIntervalMs: this.presenceTouchIntervalMs,
|
|
203
224
|
});
|
|
204
225
|
// Install the new connection in the map BEFORE closing the old one.
|
|
205
226
|
// This ensures that when the old socket's async 'close' event fires and
|
|
@@ -275,6 +296,19 @@ class DesktopCompanionRegistry {
|
|
|
275
296
|
return this.getDeviceRecordByDeviceId(userId, deviceId);
|
|
276
297
|
}
|
|
277
298
|
|
|
299
|
+
touchPresence(userId, deviceId) {
|
|
300
|
+
const userMap = this._getUserMap(userId);
|
|
301
|
+
const connection = userMap?.get(String(deviceId));
|
|
302
|
+
if (!connection?.isOpen()) return;
|
|
303
|
+
this.db.prepare(
|
|
304
|
+
`UPDATE desktop_companion_devices
|
|
305
|
+
SET status = 'online',
|
|
306
|
+
last_seen_at = datetime('now'),
|
|
307
|
+
updated_at = datetime('now')
|
|
308
|
+
WHERE user_id = ? AND device_id = ? AND revoked_at IS NULL`
|
|
309
|
+
).run(userId, deviceId);
|
|
310
|
+
}
|
|
311
|
+
|
|
278
312
|
isConnected(userId) {
|
|
279
313
|
const userMap = this._getUserMap(userId);
|
|
280
314
|
return userMap != null && userMap.size > 0;
|
|
@@ -493,7 +527,19 @@ class DesktopCompanionRegistry {
|
|
|
493
527
|
}
|
|
494
528
|
|
|
495
529
|
class DesktopCompanionConnection {
|
|
496
|
-
constructor({
|
|
530
|
+
constructor({
|
|
531
|
+
registry,
|
|
532
|
+
ws,
|
|
533
|
+
userId,
|
|
534
|
+
sessionId,
|
|
535
|
+
deviceId,
|
|
536
|
+
recordId,
|
|
537
|
+
meta,
|
|
538
|
+
timeoutMs,
|
|
539
|
+
heartbeatIntervalMs,
|
|
540
|
+
heartbeatTimeoutMs,
|
|
541
|
+
presenceTouchIntervalMs,
|
|
542
|
+
}) {
|
|
497
543
|
this.registry = registry;
|
|
498
544
|
this.ws = ws;
|
|
499
545
|
this.userId = userId;
|
|
@@ -502,11 +548,22 @@ class DesktopCompanionConnection {
|
|
|
502
548
|
this.recordId = recordId;
|
|
503
549
|
this.meta = meta || {};
|
|
504
550
|
this.timeoutMs = timeoutMs;
|
|
551
|
+
this.heartbeatIntervalMs = heartbeatIntervalMs;
|
|
552
|
+
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
|
|
553
|
+
this.presenceTouchIntervalMs = presenceTouchIntervalMs;
|
|
505
554
|
this.pending = new Map();
|
|
555
|
+
this.lastPongAt = Date.now();
|
|
556
|
+
this.lastPresenceTouchAt = 0;
|
|
557
|
+
this.heartbeatTimer = null;
|
|
506
558
|
|
|
507
559
|
ws.on('message', (data) => this._handleMessage(data));
|
|
560
|
+
ws.on('pong', () => {
|
|
561
|
+
this.lastPongAt = Date.now();
|
|
562
|
+
this.touchPresence();
|
|
563
|
+
});
|
|
508
564
|
ws.on('close', () => this._closePending(new DesktopCompanionUnavailableError('Desktop companion disconnected.')));
|
|
509
565
|
ws.on('error', (error) => this._closePending(error));
|
|
566
|
+
this._startHeartbeat();
|
|
510
567
|
}
|
|
511
568
|
|
|
512
569
|
isOpen() {
|
|
@@ -525,6 +582,21 @@ class DesktopCompanionConnection {
|
|
|
525
582
|
this._closePending(new DesktopCompanionUnavailableError('Desktop companion disconnected.'));
|
|
526
583
|
}
|
|
527
584
|
|
|
585
|
+
touchPresence({ force = false } = {}) {
|
|
586
|
+
const now = Date.now();
|
|
587
|
+
const intervalMs = Number(this.presenceTouchIntervalMs);
|
|
588
|
+
if (
|
|
589
|
+
!force
|
|
590
|
+
&& Number.isFinite(intervalMs)
|
|
591
|
+
&& intervalMs > 0
|
|
592
|
+
&& now - this.lastPresenceTouchAt < intervalMs
|
|
593
|
+
) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.lastPresenceTouchAt = now;
|
|
597
|
+
this.registry.touchPresence(this.userId, this.deviceId);
|
|
598
|
+
}
|
|
599
|
+
|
|
528
600
|
sendCommand(command, payload = {}, options = {}) {
|
|
529
601
|
if (!this.isOpen()) {
|
|
530
602
|
return Promise.reject(new DesktopCompanionUnavailableError());
|
|
@@ -549,6 +621,8 @@ class DesktopCompanionConnection {
|
|
|
549
621
|
}
|
|
550
622
|
|
|
551
623
|
_handleMessage(data) {
|
|
624
|
+
this.lastPongAt = Date.now();
|
|
625
|
+
this.touchPresence();
|
|
552
626
|
if (Buffer.isBuffer(data) && data.length > 0 && data[0] === FRAME_TYPE_VIDEO) {
|
|
553
627
|
return;
|
|
554
628
|
}
|
|
@@ -585,6 +659,10 @@ class DesktopCompanionConnection {
|
|
|
585
659
|
}
|
|
586
660
|
|
|
587
661
|
_closePending(error) {
|
|
662
|
+
if (this.heartbeatTimer) {
|
|
663
|
+
clearInterval(this.heartbeatTimer);
|
|
664
|
+
this.heartbeatTimer = null;
|
|
665
|
+
}
|
|
588
666
|
for (const pending of this.pending.values()) {
|
|
589
667
|
clearTimeout(pending.timer);
|
|
590
668
|
pending.reject(error);
|
|
@@ -596,6 +674,31 @@ class DesktopCompanionConnection {
|
|
|
596
674
|
// mark the device offline.
|
|
597
675
|
this.registry.unregisterConnection(this);
|
|
598
676
|
}
|
|
677
|
+
|
|
678
|
+
_startHeartbeat() {
|
|
679
|
+
const intervalMs = Number(this.heartbeatIntervalMs);
|
|
680
|
+
const timeoutMs = Number(this.heartbeatTimeoutMs);
|
|
681
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) return;
|
|
682
|
+
this.touchPresence({ force: true });
|
|
683
|
+
this.heartbeatTimer = setInterval(() => {
|
|
684
|
+
if (!this.isOpen()) {
|
|
685
|
+
this._closePending(new DesktopCompanionUnavailableError('Desktop companion disconnected.'));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0 && Date.now() - this.lastPongAt > timeoutMs) {
|
|
689
|
+
try { this.ws.terminate(); } catch {}
|
|
690
|
+
this._closePending(new DesktopCompanionUnavailableError('Desktop companion heartbeat timed out.'));
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.touchPresence();
|
|
694
|
+
try {
|
|
695
|
+
this.ws.ping();
|
|
696
|
+
} catch (error) {
|
|
697
|
+
this._closePending(error);
|
|
698
|
+
}
|
|
699
|
+
}, intervalMs);
|
|
700
|
+
this.heartbeatTimer.unref?.();
|
|
701
|
+
}
|
|
599
702
|
}
|
|
600
703
|
|
|
601
704
|
module.exports = {
|