neoagent 2.3.1-beta.6 → 2.3.1-beta.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/.env.example +45 -0
  2. package/docs/capabilities.md +2 -2
  3. package/docs/configuration.md +6 -5
  4. package/docs/hardware.md +1 -1
  5. package/docs/integrations.md +2 -3
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +111 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +171 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +804 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/auto/NeoAgentCarAppService.kt +114 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnection.kt +118 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnectionService.kt +86 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  30. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  31. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  32. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  33. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  35. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  36. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  37. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  38. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  39. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  43. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  44. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  45. package/flutter_app/android/app/src/main/res/values/arrays.xml +8 -0
  46. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  48. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  49. package/flutter_app/android/app/src/main/res/xml/automotive_app_desc.xml +4 -0
  50. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  51. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  52. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  53. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  54. package/flutter_app/android/build.gradle.kts +24 -0
  55. package/flutter_app/android/ci-release.keystore +0 -0
  56. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  57. package/flutter_app/android/gradle.properties +3 -0
  58. package/flutter_app/android/key.properties +4 -0
  59. package/flutter_app/android/settings.gradle.kts +26 -0
  60. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  62. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  63. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  64. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  65. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  66. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  67. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  68. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  69. package/flutter_app/lib/features/location/location_service.dart +119 -0
  70. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  71. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +569 -0
  72. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +157 -0
  73. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +233 -0
  74. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +57 -0
  75. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +218 -0
  76. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +46 -0
  77. package/flutter_app/lib/main.dart +103 -0
  78. package/flutter_app/lib/main_account_settings.dart +1250 -0
  79. package/flutter_app/lib/main_admin.dart +886 -0
  80. package/flutter_app/lib/main_app_shell.dart +2134 -0
  81. package/flutter_app/lib/main_chat.dart +3316 -0
  82. package/flutter_app/lib/main_controller.dart +6836 -0
  83. package/flutter_app/lib/main_devices.dart +2396 -0
  84. package/flutter_app/lib/main_integrations.dart +909 -0
  85. package/flutter_app/lib/main_launcher.dart +959 -0
  86. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  87. package/flutter_app/lib/main_models.dart +3539 -0
  88. package/flutter_app/lib/main_navigation.dart +188 -0
  89. package/flutter_app/lib/main_operations.dart +4851 -0
  90. package/flutter_app/lib/main_recordings.dart +920 -0
  91. package/flutter_app/lib/main_runtime.dart +838 -0
  92. package/flutter_app/lib/main_settings.dart +2040 -0
  93. package/flutter_app/lib/main_shared.dart +3147 -0
  94. package/flutter_app/lib/main_theme.dart +257 -0
  95. package/flutter_app/lib/main_voice_assistant.dart +957 -0
  96. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  97. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  98. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  99. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  100. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  101. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  102. package/flutter_app/lib/src/android_auto_bridge.dart +59 -0
  103. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  104. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  105. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  106. package/flutter_app/lib/src/backend_client.dart +1837 -0
  107. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  108. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  109. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  110. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  111. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  112. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  113. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  114. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  115. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  116. package/flutter_app/lib/src/health_bridge.dart +136 -0
  117. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  118. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  119. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  120. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  121. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  122. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  123. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  124. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  125. package/flutter_app/lib/src/oauth_launcher_io.dart +95 -0
  126. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  127. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  128. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  129. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  130. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  131. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  132. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  133. package/flutter_app/lib/src/theme/palette.dart +81 -0
  134. package/flutter_app/lib/src/web_app_update_monitor.dart +17 -0
  135. package/flutter_app/lib/src/web_app_update_monitor_stub.dart +24 -0
  136. package/flutter_app/lib/src/web_app_update_monitor_web.dart +123 -0
  137. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  138. package/flutter_app/linux/CMakeLists.txt +128 -0
  139. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  140. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +47 -0
  141. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  142. package/flutter_app/linux/flutter/generated_plugins.cmake +32 -0
  143. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  144. package/flutter_app/linux/runner/main.cc +6 -0
  145. package/flutter_app/linux/runner/my_application.cc +144 -0
  146. package/flutter_app/linux/runner/my_application.h +18 -0
  147. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  148. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  149. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  150. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +44 -0
  151. package/flutter_app/macos/Podfile +42 -0
  152. package/flutter_app/macos/Podfile.lock +87 -0
  153. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  154. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  155. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  156. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  157. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  158. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  159. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  160. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  161. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  162. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  163. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  164. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  165. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  166. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  167. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  168. package/flutter_app/macos/Runner/Info.plist +36 -0
  169. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  170. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  171. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  172. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  173. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  174. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  175. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  176. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  177. package/flutter_app/patch_strings.py +12 -0
  178. package/flutter_app/pubspec.lock +1224 -0
  179. package/flutter_app/pubspec.yaml +57 -0
  180. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  181. package/flutter_app/test/recording_payloads_test.dart +53 -0
  182. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  183. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  184. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  185. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  186. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  187. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  188. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  189. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  190. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  191. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  192. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  193. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  194. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  195. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  196. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  197. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  198. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  199. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  200. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  201. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  202. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  203. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  204. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  205. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  206. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  207. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  208. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  209. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  210. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  211. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  212. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  213. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  214. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  215. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  216. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  217. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  218. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  219. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  220. package/flutter_app/web/favicon.png +0 -0
  221. package/flutter_app/web/favicon.svg +12 -0
  222. package/flutter_app/web/icons/Icon-192.png +0 -0
  223. package/flutter_app/web/icons/Icon-512.png +0 -0
  224. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  225. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  226. package/flutter_app/web/index.html +39 -0
  227. package/flutter_app/web/manifest.json +35 -0
  228. package/flutter_app/windows/CMakeLists.txt +108 -0
  229. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  230. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +50 -0
  231. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  232. package/flutter_app/windows/flutter/generated_plugins.cmake +36 -0
  233. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  234. package/flutter_app/windows/runner/Runner.rc +121 -0
  235. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  236. package/flutter_app/windows/runner/flutter_window.h +37 -0
  237. package/flutter_app/windows/runner/main.cpp +53 -0
  238. package/flutter_app/windows/runner/resource.h +16 -0
  239. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  240. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  241. package/flutter_app/windows/runner/utils.cpp +65 -0
  242. package/flutter_app/windows/runner/utils.h +19 -0
  243. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  244. package/flutter_app/windows/runner/win32_window.h +102 -0
  245. package/lib/install_helpers.js +31 -0
  246. package/lib/manager.js +201 -6
  247. package/package.json +5 -3
  248. package/server/db/database.js +125 -1
  249. package/server/http/middleware.js +55 -2
  250. package/server/http/routes.js +1 -0
  251. package/server/http/static.js +41 -1
  252. package/server/index.js +3 -0
  253. package/server/public/.last_build_id +1 -1
  254. package/server/public/assets/AssetManifest.bin +1 -1
  255. package/server/public/assets/AssetManifest.bin.json +1 -1
  256. package/server/public/assets/NOTICES +281 -1
  257. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  258. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  259. package/server/public/canvaskit/wimp.wasm +0 -0
  260. package/server/public/flutter_bootstrap.js +2 -2
  261. package/server/public/main.dart.js +86314 -82363
  262. package/server/routes/auth.js +26 -5
  263. package/server/routes/integrations.js +108 -1
  264. package/server/routes/memory.js +11 -2
  265. package/server/routes/settings.js +84 -2
  266. package/server/routes/wearable.js +67 -0
  267. package/server/services/ai/engine.js +3 -2
  268. package/server/services/ai/models.js +30 -0
  269. package/server/services/ai/providers/githubCopilot.js +97 -0
  270. package/server/services/ai/providers/openai.js +2 -1
  271. package/server/services/ai/providers/openaiCodex.js +31 -0
  272. package/server/services/ai/settings.js +20 -0
  273. package/server/services/ai/toolSelector.js +54 -1
  274. package/server/services/ai/tools.js +88 -5
  275. package/server/services/desktop/screenRecorder.js +65 -9
  276. package/server/services/integrations/env.js +5 -0
  277. package/server/services/integrations/figma/provider.js +1 -0
  278. package/server/services/integrations/github/common.js +106 -0
  279. package/server/services/integrations/github/provider.js +499 -0
  280. package/server/services/integrations/github/repos.js +1124 -0
  281. package/server/services/integrations/google/provider.js +1 -0
  282. package/server/services/integrations/manager.js +88 -12
  283. package/server/services/integrations/microsoft/provider.js +1 -0
  284. package/server/services/integrations/oauth_provider.js +25 -8
  285. package/server/services/integrations/provider_config_store.js +85 -0
  286. package/server/services/integrations/registry.js +4 -2
  287. package/server/services/integrations/spotify/provider.js +1 -0
  288. package/server/services/integrations/trello/provider.js +842 -0
  289. package/server/services/manager.js +46 -1
  290. package/server/services/mcp/client.js +120 -23
  291. package/server/services/memory/manager.js +39 -2
  292. package/server/services/messaging/access_policy.js +10 -0
  293. package/server/services/messaging/manager.js +49 -0
  294. package/server/services/messaging/meshtastic.js +277 -0
  295. package/server/services/messaging/meshtastic_env.js +40 -0
  296. package/server/services/messaging/meshtastic_protocol.js +530 -0
  297. package/server/services/messaging/meshtastic_protocol.test.js +33 -0
  298. package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
  299. package/server/services/tasks/runtime.js +1 -1
  300. package/server/services/voice/openaiClient.js +4 -1
  301. package/server/services/voice/openaiSpeech.js +6 -1
  302. package/server/services/voice/providers.js +144 -15
  303. package/server/services/voice/runtimeManager.js +156 -19
  304. package/server/services/voice/turnRunner.js +29 -9
  305. package/server/services/wearable/firmware_manifest.js +353 -0
  306. package/server/services/wearable/gateway.js +350 -0
  307. package/server/services/wearable/protocol.js +45 -0
  308. package/server/services/wearable/service.js +240 -0
  309. package/server/services/widgets/service.js +3 -0
  310. package/server/utils/local_secrets.js +56 -0
  311. package/server/utils/logger.js +37 -9
  312. package/server/services/integrations/home_assistant/provider.js +0 -350
@@ -6,6 +6,7 @@ const { AGENT_DATA_DIR } = require('../../../runtime/paths');
6
6
  const { getOpenAiClient } = require('./openaiClient');
7
7
  const { synthesizeSpeechBuffer } = require('./openaiSpeech');
8
8
  const { transcribeChunkWithDeepgram } = require('../recordings/deepgram');
9
+ const { decryptLocalValue } = require('../../utils/local_secrets');
9
10
 
10
11
  const DEFAULT_STT_PROVIDER = 'openai';
11
12
  const DEFAULT_TTS_PROVIDER = 'openai';
@@ -36,6 +37,20 @@ const DEFAULT_GEMINI_TRANSCRIPTION_PROMPT =
36
37
  'Transcribe this audio verbatim. Return only the transcript text.';
37
38
  const EMOJI_SPEECH_REGEX =
38
39
  /[\p{Extended_Pictographic}\p{Emoji_Presentation}\p{Regional_Indicator}\u200D\uFE0F\u20E3]/gu;
40
+ const WEARABLE_SAFE_AUDIO_FORMAT = Object.freeze({
41
+ responseFormat: 'wav',
42
+ mimeType: 'audio/wav',
43
+ streamResponseFormat: 'pcm',
44
+ streamMimeType: 'audio/wav',
45
+ pcmSampleRate: 24000,
46
+ pcmChannels: 1,
47
+ pcmBitsPerSample: 16,
48
+ deepgramEncoding: 'linear16',
49
+ deepgramContainer: 'wav',
50
+ deepgramStreamContainer: 'none',
51
+ });
52
+ const MIN_STREAM_PCM_CHUNK_BYTES = 24000;
53
+ const MAX_STREAM_PCM_CHUNK_BYTES = 48000;
39
54
 
40
55
  function withTimeout(promise, timeoutMs, label) {
41
56
  const normalizedTimeout = Number(timeoutMs);
@@ -94,7 +109,7 @@ function resolveApiKey(candidates = []) {
94
109
  const snake = lower.replace(/[^a-z0-9]+/g, '_');
95
110
  const variants = [key, lower, snake];
96
111
  for (const variant of variants) {
97
- const value = keys[variant];
112
+ const value = decryptLocalValue(keys[variant]);
98
113
  if (typeof value === 'string' && value.trim()) {
99
114
  return value.trim();
100
115
  }
@@ -138,9 +153,16 @@ function normalizeVoiceSynthesisOptions(options = {}) {
138
153
  provider,
139
154
  model: resolveTtsModel(provider, options.model),
140
155
  voice: resolveTtsVoice(provider, options.voice),
156
+ responseFormat: String(options.responseFormat || '').trim().toLowerCase(),
157
+ transport: String(options.transport || '').trim().toLowerCase(),
141
158
  };
142
159
  }
143
160
 
161
+ function resolveWearableSafeAudioOptions(options = {}) {
162
+ return String(options.transport || '').trim().toLowerCase() === 'wearable'
163
+ || String(options.responseFormat || '').trim().toLowerCase() === 'wav';
164
+ }
165
+
144
166
  function requireApiKey(settingLabel, candidates = []) {
145
167
  const apiKey = resolveApiKey(candidates);
146
168
  if (!apiKey) {
@@ -184,10 +206,11 @@ async function fetchAudioStreamOrThrow(url, init, errorPrefix, defaultMimeType =
184
206
  while (true) {
185
207
  const { done, value } = await reader.read();
186
208
  if (done) break;
187
- if (value?.length) chunks.push(Buffer.from(value));
209
+ if (value?.length) {
210
+ chunks.push(Buffer.from(value));
211
+ }
188
212
  }
189
- const audioBytes = Buffer.concat(chunks);
190
- await onChunk({ audioBytes, mimeType });
213
+ await onChunk({ audioBytes: Buffer.concat(chunks), mimeType });
191
214
  }
192
215
 
193
216
  function guessExtFromMimeType(mimeType) {
@@ -248,6 +271,49 @@ function wrapPcmAsWav(audioBytes, format) {
248
271
  return Buffer.concat([header, data]);
249
272
  }
250
273
 
274
+ async function streamPcmAsWavChunks(readable, format, onChunk) {
275
+ const source = readable && typeof readable.getReader === 'function'
276
+ ? readable
277
+ : null;
278
+ let pending = Buffer.alloc(0);
279
+
280
+ async function flushPending(force = false) {
281
+ while (pending.length >= MIN_STREAM_PCM_CHUNK_BYTES || (force && pending.length > 0)) {
282
+ const targetLength = force
283
+ ? pending.length
284
+ : Math.min(pending.length, MAX_STREAM_PCM_CHUNK_BYTES);
285
+ const evenLength = targetLength - (targetLength % 2);
286
+ if (evenLength <= 0) return;
287
+ const pcmChunk = pending.subarray(0, evenLength);
288
+ pending = pending.subarray(evenLength);
289
+ await onChunk({
290
+ audioBytes: wrapPcmAsWav(pcmChunk, format),
291
+ mimeType: WEARABLE_SAFE_AUDIO_FORMAT.streamMimeType,
292
+ });
293
+ }
294
+ }
295
+
296
+ if (source) {
297
+ const reader = source.getReader();
298
+ while (true) {
299
+ const { done, value } = await reader.read();
300
+ if (done) break;
301
+ if (value?.length) {
302
+ pending = Buffer.concat([pending, Buffer.from(value)]);
303
+ await flushPending(false);
304
+ }
305
+ }
306
+ } else {
307
+ for await (const chunk of readable) {
308
+ if (chunk?.length) {
309
+ pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
310
+ await flushPending(false);
311
+ }
312
+ }
313
+ }
314
+ await flushPending(true);
315
+ }
316
+
251
317
  async function transcribeWithOpenAi(filePath, model, options = {}) {
252
318
  const client = getOpenAiClient({
253
319
  apiKey: typeof options.apiKey === 'string' ? options.apiKey.trim() : '',
@@ -341,10 +407,15 @@ async function synthesizeWithOpenAi(text, model, voice, options = {}) {
341
407
  if (!client) {
342
408
  throw new Error('OpenAI TTS is selected but OPENAI_API_KEY is not configured.');
343
409
  }
344
- const audioBytes = await synthesizeSpeechBuffer(client, text, { model, voice });
410
+ const useWearableSafeAudio = resolveWearableSafeAudioOptions(options);
411
+ const audioBytes = await synthesizeSpeechBuffer(client, text, {
412
+ model,
413
+ voice,
414
+ responseFormat: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.responseFormat : 'mp3',
415
+ });
345
416
  return {
346
417
  audioBytes,
347
- mimeType: 'audio/mpeg',
418
+ mimeType: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.mimeType : 'audio/mpeg',
348
419
  };
349
420
  }
350
421
 
@@ -356,25 +427,49 @@ async function streamWithOpenAi(text, model, voice, options = {}, onChunk) {
356
427
  if (!client) {
357
428
  throw new Error('OpenAI TTS is selected but OPENAI_API_KEY is not configured.');
358
429
  }
430
+ const useWearableSafeAudio = resolveWearableSafeAudioOptions(options);
359
431
  const response = await client.audio.speech.create({
360
432
  model: String(model || 'gpt-4o-mini-tts').trim() || 'gpt-4o-mini-tts',
361
433
  voice: String(voice || 'alloy').trim() || 'alloy',
362
434
  input: text,
363
- response_format: 'mp3',
435
+ response_format: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.streamResponseFormat : 'mp3',
364
436
  });
437
+ if (useWearableSafeAudio) {
438
+ await streamPcmAsWavChunks(
439
+ response.body,
440
+ {
441
+ bitsPerSample: WEARABLE_SAFE_AUDIO_FORMAT.pcmBitsPerSample,
442
+ sampleRate: WEARABLE_SAFE_AUDIO_FORMAT.pcmSampleRate,
443
+ channels: WEARABLE_SAFE_AUDIO_FORMAT.pcmChannels,
444
+ },
445
+ onChunk,
446
+ );
447
+ return;
448
+ }
365
449
  const chunks = [];
366
450
  for await (const chunk of response.body) {
367
451
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
368
452
  }
369
453
  const audioBytes = Buffer.concat(chunks);
370
- await onChunk({ audioBytes, mimeType: 'audio/mpeg' });
454
+ await onChunk({
455
+ audioBytes,
456
+ mimeType: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.mimeType : 'audio/mpeg',
457
+ });
371
458
  }
372
459
 
373
- async function synthesizeWithDeepgram(text, model) {
460
+ async function synthesizeWithDeepgram(text, model, options = {}) {
374
461
  const apiKey = requireApiKey('Deepgram TTS', ['DEEPGRAM_API_KEY']);
462
+ const useWearableSafeAudio = resolveWearableSafeAudioOptions(options);
463
+ const searchParams = new URLSearchParams({
464
+ model,
465
+ });
466
+ if (useWearableSafeAudio) {
467
+ searchParams.set('encoding', WEARABLE_SAFE_AUDIO_FORMAT.deepgramEncoding);
468
+ searchParams.set('container', WEARABLE_SAFE_AUDIO_FORMAT.deepgramContainer);
469
+ }
375
470
 
376
471
  return fetchAudioOrThrow(
377
- `https://api.deepgram.com/v1/speak?model=${encodeURIComponent(model)}`,
472
+ `https://api.deepgram.com/v1/speak?${searchParams.toString()}`,
378
473
  {
379
474
  method: 'POST',
380
475
  headers: {
@@ -384,13 +479,47 @@ async function synthesizeWithDeepgram(text, model) {
384
479
  body: JSON.stringify({ text }),
385
480
  },
386
481
  'Deepgram TTS request failed',
482
+ useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.mimeType : 'audio/mpeg',
387
483
  );
388
484
  }
389
485
 
390
- async function streamWithDeepgram(text, model, onChunk) {
486
+ async function streamWithDeepgram(text, model, options = {}, onChunk) {
391
487
  const apiKey = requireApiKey('Deepgram TTS', ['DEEPGRAM_API_KEY']);
488
+ const useWearableSafeAudio = resolveWearableSafeAudioOptions(options);
489
+ const searchParams = new URLSearchParams({
490
+ model,
491
+ });
492
+ if (useWearableSafeAudio) {
493
+ searchParams.set('encoding', WEARABLE_SAFE_AUDIO_FORMAT.deepgramEncoding);
494
+ searchParams.set('container', WEARABLE_SAFE_AUDIO_FORMAT.deepgramStreamContainer);
495
+ searchParams.set('sample_rate', String(WEARABLE_SAFE_AUDIO_FORMAT.pcmSampleRate));
496
+ const response = await fetch(
497
+ `https://api.deepgram.com/v1/speak?${searchParams.toString()}`,
498
+ {
499
+ method: 'POST',
500
+ headers: {
501
+ Authorization: `Token ${apiKey}`,
502
+ 'Content-Type': 'application/json',
503
+ },
504
+ body: JSON.stringify({ text }),
505
+ },
506
+ );
507
+ if (!response.ok) {
508
+ await throwResponseError(response, 'Deepgram TTS stream failed');
509
+ }
510
+ await streamPcmAsWavChunks(
511
+ response.body,
512
+ {
513
+ bitsPerSample: WEARABLE_SAFE_AUDIO_FORMAT.pcmBitsPerSample,
514
+ sampleRate: WEARABLE_SAFE_AUDIO_FORMAT.pcmSampleRate,
515
+ channels: WEARABLE_SAFE_AUDIO_FORMAT.pcmChannels,
516
+ },
517
+ onChunk,
518
+ );
519
+ return;
520
+ }
392
521
  await fetchAudioStreamOrThrow(
393
- `https://api.deepgram.com/v1/speak?model=${encodeURIComponent(model)}`,
522
+ `https://api.deepgram.com/v1/speak?${searchParams.toString()}`,
394
523
  {
395
524
  method: 'POST',
396
525
  headers: {
@@ -400,7 +529,7 @@ async function streamWithDeepgram(text, model, onChunk) {
400
529
  body: JSON.stringify({ text }),
401
530
  },
402
531
  'Deepgram TTS stream failed',
403
- 'audio/mpeg',
532
+ useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.mimeType : 'audio/mpeg',
404
533
  onChunk,
405
534
  );
406
535
  }
@@ -586,7 +715,7 @@ async function synthesizeVoiceReply(text, options = {}) {
586
715
  if (provider === 'openai') {
587
716
  request = synthesizeWithOpenAi(content, model, voice, options);
588
717
  } else if (provider === 'deepgram') {
589
- request = synthesizeWithDeepgram(content, model);
718
+ request = synthesizeWithDeepgram(content, model, options);
590
719
  } else {
591
720
  request = synthesizeWithGemini(content, model, voice, options);
592
721
  }
@@ -668,7 +797,7 @@ async function synthesizeVoiceReplyStream(text, options = {}, onChunk) {
668
797
  if (provider === 'openai') {
669
798
  await streamWithOpenAi(chunk, model, voice, options, onChunk);
670
799
  } else if (provider === 'deepgram') {
671
- await streamWithDeepgram(chunk, model, onChunk);
800
+ await streamWithDeepgram(chunk, model, options, onChunk);
672
801
  } else {
673
802
  await streamWithGemini(chunk, model, voice, options, onChunk);
674
803
  }
@@ -34,11 +34,17 @@ class VoiceRuntimeManager {
34
34
  return this.sessions.get(String(sessionId || '').trim()) || null;
35
35
  }
36
36
 
37
- async openFlutterSession({ userId, agentId = null, socket, sessionId = null } = {}) {
38
- if (!socket) {
39
- throw new Error('Socket is required to open a Flutter voice session.');
37
+ async openSession({
38
+ userId,
39
+ agentId = null,
40
+ sessionId = null,
41
+ platform = 'voice_live',
42
+ sink,
43
+ outputMode = 'audio_and_text',
44
+ } = {}) {
45
+ if (!sink) {
46
+ throw new Error('A voice session sink is required.');
40
47
  }
41
-
42
48
  const voiceSettings = getVoiceRuntimeSettings(userId, agentId);
43
49
  const liveProviderRuntime = this.#getProviderRuntime(
44
50
  userId,
@@ -53,16 +59,37 @@ class VoiceRuntimeManager {
53
59
  id: resolvedSessionId,
54
60
  userId,
55
61
  agentId,
56
- platform: 'voice_live',
62
+ platform,
57
63
  voiceSettings: {
58
64
  ...voiceSettings,
59
65
  liveApiKey: liveProviderRuntime.apiKey,
60
66
  liveBaseUrl: liveProviderRuntime.baseUrl,
61
67
  },
68
+ sink,
69
+ outputMode,
70
+ });
71
+
72
+ session.adapter = adapter;
73
+ this.sessions.set(resolvedSessionId, session);
74
+ await session.publishReady();
75
+ return session;
76
+ }
77
+
78
+ async openFlutterSession({ userId, agentId = null, socket, sessionId = null } = {}) {
79
+ if (!socket) {
80
+ throw new Error('Socket is required to open a Flutter voice session.');
81
+ }
82
+ return this.openSession({
83
+ userId,
84
+ agentId,
85
+ sessionId,
86
+ platform: 'voice_live',
87
+ outputMode: 'audio_and_text',
62
88
  sink: {
63
89
  publishReady: async (_session, extra = {}) => {
90
+ const voiceSettings = getVoiceRuntimeSettings(userId, agentId);
64
91
  socket.emit('voice:session_ready', {
65
- sessionId: resolvedSessionId,
92
+ sessionId: String(sessionId || _session.id).trim(),
66
93
  runtimeMode: voiceSettings.runtimeMode,
67
94
  provider: voiceSettings.liveProvider,
68
95
  model: voiceSettings.liveModel,
@@ -72,52 +99,58 @@ class VoiceRuntimeManager {
72
99
  },
73
100
  setState: async (_session, state, extra = {}) => {
74
101
  socket.emit('voice:assistant_state', {
75
- sessionId: resolvedSessionId,
102
+ sessionId: _session.id,
76
103
  state,
77
104
  ...extra,
78
105
  });
79
106
  },
80
107
  publishTranscriptPartial: async (_session, content) => {
81
108
  socket.emit('voice:transcript_partial', {
82
- sessionId: resolvedSessionId,
109
+ sessionId: _session.id,
83
110
  content,
84
111
  });
85
112
  },
86
113
  publishTranscriptFinal: async (_session, content) => {
87
114
  socket.emit('voice:transcript_final', {
88
- sessionId: resolvedSessionId,
115
+ sessionId: _session.id,
89
116
  content,
90
117
  });
91
118
  },
92
119
  publishAssistantOutput: async (_session, content, options = {}) => {
93
- await this.#deliverFlutterAssistantOutput(socket, resolvedSessionId, session, content, options);
120
+ await this.#deliverFlutterAssistantOutput(socket, _session.id, _session, content, options);
94
121
  },
95
- interruptOutput: async () => {
122
+ interruptOutput: async (_session) => {
96
123
  socket.emit('voice:assistant_state', {
97
- sessionId: resolvedSessionId,
124
+ sessionId: _session.id,
98
125
  state: 'interrupted',
99
126
  });
100
127
  },
101
128
  publishError: async (_session, message, extra = {}) => {
102
129
  socket.emit('voice:error', {
103
- sessionId: resolvedSessionId,
130
+ sessionId: _session.id,
104
131
  error: message,
105
132
  ...extra,
106
133
  });
107
134
  },
108
- close: async () => {
135
+ close: async (_session) => {
109
136
  socket.emit('voice:assistant_state', {
110
- sessionId: resolvedSessionId,
137
+ sessionId: _session.id,
111
138
  state: 'closed',
112
139
  });
113
140
  },
114
141
  },
115
142
  });
143
+ }
116
144
 
117
- session.adapter = adapter;
118
- this.sessions.set(resolvedSessionId, session);
119
- await session.publishReady();
120
- return session;
145
+ async openWearableSession({ userId, agentId = null, sessionId = null, sink } = {}) {
146
+ return this.openSession({
147
+ userId,
148
+ agentId,
149
+ sessionId,
150
+ platform: 'wearable_live',
151
+ outputMode: 'audio_and_text',
152
+ sink,
153
+ });
121
154
  }
122
155
 
123
156
  async closeSession(sessionId, reason = 'closed') {
@@ -446,6 +479,7 @@ class VoiceRuntimeManager {
446
479
  for (const attempt of ttsAttempts) {
447
480
  index = 0;
448
481
  streamError = null;
482
+ let attemptChunks = 0;
449
483
  try {
450
484
  await synthesizeVoiceReplyStream(
451
485
  spokenContent,
@@ -459,13 +493,21 @@ class VoiceRuntimeManager {
459
493
  audioBase64: audioBytes.toString('base64'),
460
494
  mimeType,
461
495
  });
496
+ attemptChunks += 1;
462
497
  index += 1;
463
498
  },
464
499
  );
500
+ if (attemptChunks === 0) {
501
+ throw new Error(`${attempt.provider} TTS produced no audio chunks.`);
502
+ }
465
503
  streamError = null;
466
504
  break;
467
505
  } catch (error) {
468
506
  streamError = String(error?.message || error || 'Voice playback failed.');
507
+ console.warn(`[VoiceRuntime] ${attempt.provider} TTS failed for flutter session ${sessionId}: ${streamError}`);
508
+ if (attemptChunks > 0) {
509
+ break;
510
+ }
469
511
  }
470
512
  }
471
513
  } catch (error) {
@@ -490,6 +532,99 @@ class VoiceRuntimeManager {
490
532
  }
491
533
  }
492
534
 
535
+ async deliverWearableAssistantOutput(ws, sessionId, content, options = {}) {
536
+ const session = this.getSession(sessionId);
537
+ if (!session) {
538
+ return;
539
+ }
540
+ const kind = String(options.kind || 'final').trim() || 'final';
541
+ ws.send(JSON.stringify({
542
+ type: 'voice:assistant_text',
543
+ sessionId,
544
+ content,
545
+ kind,
546
+ }));
547
+
548
+ if (kind === 'final') {
549
+ await session.setState('speaking', { kind });
550
+ }
551
+
552
+ const voiceOptions = normalizeVoiceSynthesisOptions({
553
+ provider: session.voiceSettings?.liveProvider,
554
+ model: session.voiceSettings?.liveTtsModel,
555
+ voice: session.voiceSettings?.liveVoice,
556
+ transport: 'wearable',
557
+ responseFormat: 'wav',
558
+ });
559
+ const spokenContent = sanitizeSpeechText(content);
560
+ let index = 0;
561
+ let streamError = null;
562
+ const ttsAttempts = this.#buildTtsAttemptOrder(session, voiceOptions);
563
+
564
+ if (spokenContent) {
565
+ try {
566
+ for (const attempt of ttsAttempts) {
567
+ index = 0;
568
+ streamError = null;
569
+ let attemptChunks = 0;
570
+ try {
571
+ await synthesizeVoiceReplyStream(
572
+ spokenContent,
573
+ attempt,
574
+ async ({ audioBytes, mimeType }) => {
575
+ if (session.closed || session.interrupted) return;
576
+ ws.send(JSON.stringify({
577
+ type: 'voice:audio_chunk',
578
+ sessionId,
579
+ kind,
580
+ index,
581
+ audioBase64: audioBytes.toString('base64'),
582
+ mimeType,
583
+ }));
584
+ attemptChunks += 1;
585
+ index += 1;
586
+ },
587
+ );
588
+ if (attemptChunks === 0) {
589
+ throw new Error(`${attempt.provider} TTS produced no audio chunks.`);
590
+ }
591
+ break;
592
+ } catch (error) {
593
+ streamError = String(error?.message || error || 'Voice playback failed.');
594
+ console.warn(`[VoiceRuntime] ${attempt.provider} TTS failed for wearable session ${sessionId}: ${streamError}`);
595
+ if (attemptChunks > 0) {
596
+ break;
597
+ }
598
+ }
599
+ }
600
+ } catch (error) {
601
+ streamError = String(error?.message || error || 'Voice playback failed.');
602
+ }
603
+ }
604
+
605
+ if (!streamError && !session.closed && !session.interrupted) {
606
+ ws.send(JSON.stringify({
607
+ type: 'voice:audio_done',
608
+ sessionId,
609
+ kind,
610
+ totalChunks: index,
611
+ }));
612
+ } else if (kind === 'final' && !session.closed && !session.interrupted) {
613
+ ws.send(JSON.stringify({
614
+ type: 'voice:error',
615
+ sessionId,
616
+ error: streamError,
617
+ recoverable: true,
618
+ phase: 'tts',
619
+ }));
620
+ await session.setState('degraded', { kind, phase: 'tts' });
621
+ }
622
+
623
+ if (kind === 'final' && !streamError) {
624
+ await session.setState('idle');
625
+ }
626
+ }
627
+
493
628
  #buildTtsAttemptOrder(session, voiceOptions) {
494
629
  const attempts = [];
495
630
  const providers = [
@@ -501,6 +636,8 @@ class VoiceRuntimeManager {
501
636
  provider,
502
637
  model: provider === voiceOptions.provider ? voiceOptions.model : null,
503
638
  voice: provider === voiceOptions.provider ? voiceOptions.voice : null,
639
+ transport: voiceOptions.transport,
640
+ responseFormat: voiceOptions.responseFormat,
504
641
  });
505
642
  const runtime = provider === voiceOptions.provider
506
643
  ? {
@@ -110,8 +110,35 @@ async function runVoiceTranscriptTurn({
110
110
  });
111
111
 
112
112
  const replyText = String(runResult?.content || '').trim();
113
+ const assistantMetadata = {
114
+ ...persistedMetadata,
115
+ platform,
116
+ tokens: runResult?.totalTokens || 0,
117
+ screenshotIncluded: Boolean(screenshotContext),
118
+ screenshotVisionProvider: screenshotContext?.provider || null,
119
+ screenshotVisionModel: screenshotContext?.model || null,
120
+ };
113
121
  if (!replyText) {
114
- throw new Error('Agent returned an empty voice reply.');
122
+ db.prepare('INSERT INTO conversation_history (user_id, agent_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?, ?)')
123
+ .run(
124
+ userId,
125
+ agentId,
126
+ runResult?.runId || null,
127
+ 'assistant',
128
+ '',
129
+ JSON.stringify(assistantMetadata),
130
+ );
131
+ return {
132
+ runId: runResult?.runId || null,
133
+ transcript: transcriptText,
134
+ replyText: '',
135
+ ttsProvider: voiceOptions.provider,
136
+ ttsModel: voiceOptions.model,
137
+ ttsVoice: voiceOptions.voice,
138
+ audioMimeType: 'audio/mpeg',
139
+ audioBase64: '',
140
+ ttsError: null,
141
+ };
115
142
  }
116
143
 
117
144
  db.prepare('INSERT INTO conversation_history (user_id, agent_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?, ?)')
@@ -121,14 +148,7 @@ async function runVoiceTranscriptTurn({
121
148
  runResult?.runId || null,
122
149
  'assistant',
123
150
  replyText,
124
- JSON.stringify({
125
- ...persistedMetadata,
126
- platform,
127
- tokens: runResult?.totalTokens || 0,
128
- screenshotIncluded: Boolean(screenshotContext),
129
- screenshotVisionProvider: screenshotContext?.provider || null,
130
- screenshotVisionModel: screenshotContext?.model || null,
131
- }),
151
+ JSON.stringify(assistantMetadata),
132
152
  );
133
153
 
134
154
  let synthesized;