@mentra/engine 0.1.0-beta.0 → 0.1.0-dev.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.
Files changed (355) hide show
  1. package/README.md +42 -1
  2. package/build/engine.d.ts +11 -3
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -2
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/dev.d.ts.map +1 -1
  7. package/build/facades/dev.js +9 -5
  8. package/build/facades/dev.js.map +1 -1
  9. package/build/facades/gallery.js +1 -1
  10. package/build/facades/gallery.js.map +1 -1
  11. package/build/facades/glasses.d.ts +1 -0
  12. package/build/facades/glasses.d.ts.map +1 -1
  13. package/build/facades/glasses.js +19 -4
  14. package/build/facades/glasses.js.map +1 -1
  15. package/build/facades/glassesSettings.d.ts.map +1 -1
  16. package/build/facades/glassesSettings.js +1 -0
  17. package/build/facades/glassesSettings.js.map +1 -1
  18. package/build/facades/miniapps.d.ts +4 -0
  19. package/build/facades/miniapps.d.ts.map +1 -1
  20. package/build/facades/miniapps.js +5 -0
  21. package/build/facades/miniapps.js.map +1 -1
  22. package/build/facades/ota.d.ts +11 -2
  23. package/build/facades/ota.d.ts.map +1 -1
  24. package/build/facades/ota.js +19 -4
  25. package/build/facades/ota.js.map +1 -1
  26. package/build/facades/pairing.d.ts +4 -0
  27. package/build/facades/pairing.d.ts.map +1 -1
  28. package/build/facades/pairing.js +10 -8
  29. package/build/facades/pairing.js.map +1 -1
  30. package/build/facades/phoneNotifications.d.ts +0 -4
  31. package/build/facades/phoneNotifications.d.ts.map +1 -1
  32. package/build/facades/phoneNotifications.js +4 -14
  33. package/build/facades/phoneNotifications.js.map +1 -1
  34. package/build/index.d.ts +6 -3
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +4 -2
  37. package/build/index.js.map +1 -1
  38. package/build/internal.d.ts +2 -2
  39. package/build/internal.d.ts.map +1 -1
  40. package/build/internal.js +1 -1
  41. package/build/internal.js.map +1 -1
  42. package/build/react/MentraLiveOtaFlow.d.ts +38 -0
  43. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  44. package/build/react/MentraLiveOtaFlow.js +528 -0
  45. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  46. package/build/react/index.d.ts +2 -0
  47. package/build/react/index.d.ts.map +1 -0
  48. package/build/react/index.js +2 -0
  49. package/build/react/index.js.map +1 -0
  50. package/build/react/useEngineSnapshot.d.ts +2 -0
  51. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  52. package/build/react/useEngineSnapshot.js +14 -0
  53. package/build/react/useEngineSnapshot.js.map +1 -0
  54. package/build/runtime/bootstrap.d.ts +24 -1
  55. package/build/runtime/bootstrap.d.ts.map +1 -1
  56. package/build/runtime/bootstrap.js +17 -0
  57. package/build/runtime/bootstrap.js.map +1 -1
  58. package/build/runtime/scanQrSeam.d.ts +7 -0
  59. package/build/runtime/scanQrSeam.d.ts.map +1 -0
  60. package/build/runtime/scanQrSeam.js +16 -0
  61. package/build/runtime/scanQrSeam.js.map +1 -0
  62. package/build/services/AppRegistry.d.ts +18 -8
  63. package/build/services/AppRegistry.d.ts.map +1 -1
  64. package/build/services/AppRegistry.js +44 -45
  65. package/build/services/AppRegistry.js.map +1 -1
  66. package/build/services/AudioCloudUplink.d.ts +9 -3
  67. package/build/services/AudioCloudUplink.d.ts.map +1 -1
  68. package/build/services/AudioCloudUplink.js +9 -3
  69. package/build/services/AudioCloudUplink.js.map +1 -1
  70. package/build/services/AudioPlaybackService.d.ts +9 -1
  71. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  72. package/build/services/AudioPlaybackService.js +77 -32
  73. package/build/services/AudioPlaybackService.js.map +1 -1
  74. package/build/services/BlobStore.d.ts +2 -1
  75. package/build/services/BlobStore.d.ts.map +1 -1
  76. package/build/services/BlobStore.js +17 -9
  77. package/build/services/BlobStore.js.map +1 -1
  78. package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
  79. package/build/services/CloudAudioSubscriptionSync.js +13 -1
  80. package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
  81. package/build/services/CloudClientService.d.ts +15 -0
  82. package/build/services/CloudClientService.d.ts.map +1 -1
  83. package/build/services/CloudClientService.js +75 -0
  84. package/build/services/CloudClientService.js.map +1 -1
  85. package/build/services/ForegroundLocationPermission.d.ts +13 -0
  86. package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
  87. package/build/services/ForegroundLocationPermission.js +12 -0
  88. package/build/services/ForegroundLocationPermission.js.map +1 -0
  89. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  90. package/build/services/GlassesSettingsSync.js +9 -3
  91. package/build/services/GlassesSettingsSync.js.map +1 -1
  92. package/build/services/GlassesStatusProjection.d.ts +2 -1
  93. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  94. package/build/services/GlassesStatusProjection.js +15 -8
  95. package/build/services/GlassesStatusProjection.js.map +1 -1
  96. package/build/services/HotspotOtaTransport.d.ts +29 -0
  97. package/build/services/HotspotOtaTransport.d.ts.map +1 -0
  98. package/build/services/HotspotOtaTransport.js +146 -0
  99. package/build/services/HotspotOtaTransport.js.map +1 -0
  100. package/build/services/HotspotShutdown.d.ts +17 -0
  101. package/build/services/HotspotShutdown.d.ts.map +1 -0
  102. package/build/services/HotspotShutdown.js +29 -0
  103. package/build/services/HotspotShutdown.js.map +1 -0
  104. package/build/services/LocalDisplayManager.d.ts +15 -0
  105. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  106. package/build/services/LocalDisplayManager.js +106 -20
  107. package/build/services/LocalDisplayManager.js.map +1 -1
  108. package/build/services/LocalMiniappRuntime.d.ts +67 -1
  109. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  110. package/build/services/LocalMiniappRuntime.js +486 -126
  111. package/build/services/LocalMiniappRuntime.js.map +1 -1
  112. package/build/services/LocalMiniappStorage.d.ts +32 -0
  113. package/build/services/LocalMiniappStorage.d.ts.map +1 -0
  114. package/build/services/LocalMiniappStorage.js +66 -0
  115. package/build/services/LocalMiniappStorage.js.map +1 -0
  116. package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
  117. package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
  118. package/build/services/LocalMiniappUserIdentity.js +43 -0
  119. package/build/services/LocalMiniappUserIdentity.js.map +1 -0
  120. package/build/services/MicStateCoordinator.d.ts +42 -1
  121. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  122. package/build/services/MicStateCoordinator.js +141 -3
  123. package/build/services/MicStateCoordinator.js.map +1 -1
  124. package/build/services/MiniappLauncher.d.ts.map +1 -1
  125. package/build/services/MiniappLauncher.js +23 -3
  126. package/build/services/MiniappLauncher.js.map +1 -1
  127. package/build/services/MiniappLiveness.d.ts +12 -0
  128. package/build/services/MiniappLiveness.d.ts.map +1 -0
  129. package/build/services/MiniappLiveness.js +16 -0
  130. package/build/services/MiniappLiveness.js.map +1 -0
  131. package/build/services/OtaArtifactDownloader.d.ts +65 -0
  132. package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
  133. package/build/services/OtaArtifactDownloader.js +226 -0
  134. package/build/services/OtaArtifactDownloader.js.map +1 -0
  135. package/build/services/OtaAutoChain.d.ts +34 -0
  136. package/build/services/OtaAutoChain.d.ts.map +1 -0
  137. package/build/services/OtaAutoChain.js +83 -0
  138. package/build/services/OtaAutoChain.js.map +1 -0
  139. package/build/services/OtaErrorMapping.d.ts +18 -0
  140. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  141. package/build/services/OtaErrorMapping.js +63 -0
  142. package/build/services/OtaErrorMapping.js.map +1 -0
  143. package/build/services/OtaInstallCoordinator.d.ts +94 -14
  144. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  145. package/build/services/OtaInstallCoordinator.js +574 -130
  146. package/build/services/OtaInstallCoordinator.js.map +1 -1
  147. package/build/services/OtaService.d.ts.map +1 -1
  148. package/build/services/OtaService.js +8 -1
  149. package/build/services/OtaService.js.map +1 -1
  150. package/build/services/OtaUpdateCheckService.d.ts +38 -3
  151. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  152. package/build/services/OtaUpdateCheckService.js +142 -22
  153. package/build/services/OtaUpdateCheckService.js.map +1 -1
  154. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  155. package/build/services/PhoneCalendarService.js +22 -1
  156. package/build/services/PhoneCalendarService.js.map +1 -1
  157. package/build/services/PhoneCameraFovCoordinator.d.ts +2 -0
  158. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
  159. package/build/services/PhoneCameraFovCoordinator.js +14 -0
  160. package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
  161. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  162. package/build/services/PhoneNotificationsSync.js +12 -10
  163. package/build/services/PhoneNotificationsSync.js.map +1 -1
  164. package/build/services/PhonePhotoCoordinator.d.ts +10 -3
  165. package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
  166. package/build/services/PhonePhotoCoordinator.js +21 -3
  167. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  168. package/build/services/PhoneStreamCoordinator.d.ts +8 -1
  169. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  170. package/build/services/PhoneStreamCoordinator.js +125 -13
  171. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  172. package/build/services/PhoneVideoCoordinator.d.ts +2 -0
  173. package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
  174. package/build/services/PhoneVideoCoordinator.js +10 -0
  175. package/build/services/PhoneVideoCoordinator.js.map +1 -1
  176. package/build/services/SceneRenderer.d.ts.map +1 -1
  177. package/build/services/SceneRenderer.js +15 -0
  178. package/build/services/SceneRenderer.js.map +1 -1
  179. package/build/services/SupportProfileSync.d.ts +22 -0
  180. package/build/services/SupportProfileSync.d.ts.map +1 -0
  181. package/build/services/SupportProfileSync.js +213 -0
  182. package/build/services/SupportProfileSync.js.map +1 -0
  183. package/build/services/TtsTextSanitizer.d.ts +5 -0
  184. package/build/services/TtsTextSanitizer.d.ts.map +1 -0
  185. package/build/services/TtsTextSanitizer.js +61 -0
  186. package/build/services/TtsTextSanitizer.js.map +1 -0
  187. package/build/services/WifiAdbRequest.d.ts +17 -0
  188. package/build/services/WifiAdbRequest.d.ts.map +1 -0
  189. package/build/services/WifiAdbRequest.js +35 -0
  190. package/build/services/WifiAdbRequest.js.map +1 -0
  191. package/build/services/asg/asgCameraApi.d.ts +3 -0
  192. package/build/services/asg/asgCameraApi.d.ts.map +1 -1
  193. package/build/services/asg/asgCameraApi.js +72 -2
  194. package/build/services/asg/asgCameraApi.js.map +1 -1
  195. package/build/services/asg/gallerySyncService.d.ts.map +1 -1
  196. package/build/services/asg/gallerySyncService.js +26 -14
  197. package/build/services/asg/gallerySyncService.js.map +1 -1
  198. package/build/services/asg/localNetworkTransport.d.ts +2 -2
  199. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  200. package/build/services/asg/localNetworkTransport.js +6 -6
  201. package/build/services/asg/localNetworkTransport.js.map +1 -1
  202. package/build/services/audioPlaybackAssets.d.ts +8 -0
  203. package/build/services/audioPlaybackAssets.d.ts.map +1 -0
  204. package/build/services/audioPlaybackAssets.js +7 -0
  205. package/build/services/audioPlaybackAssets.js.map +1 -0
  206. package/build/services/glassesClockSync.js +2 -2
  207. package/build/services/glassesClockSync.js.map +1 -1
  208. package/build/services/manifestActions.d.ts +7 -0
  209. package/build/services/manifestActions.d.ts.map +1 -0
  210. package/build/services/manifestActions.js +25 -0
  211. package/build/services/manifestActions.js.map +1 -0
  212. package/build/services/otaDisplayState.d.ts +22 -11
  213. package/build/services/otaDisplayState.d.ts.map +1 -1
  214. package/build/services/otaDisplayState.js +36 -14
  215. package/build/services/otaDisplayState.js.map +1 -1
  216. package/build/services/otaInstallPolicy.d.ts +15 -3
  217. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  218. package/build/services/otaInstallPolicy.js +24 -3
  219. package/build/services/otaInstallPolicy.js.map +1 -1
  220. package/build/services/otaManifestUrl.d.ts +2 -0
  221. package/build/services/otaManifestUrl.d.ts.map +1 -0
  222. package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
  223. package/build/services/otaManifestUrl.js.map +1 -0
  224. package/build/services/slimStreamStatus.d.ts +11 -0
  225. package/build/services/slimStreamStatus.d.ts.map +1 -1
  226. package/build/services/slimStreamStatus.js +14 -4
  227. package/build/services/slimStreamStatus.js.map +1 -1
  228. package/build/stores/apps.d.ts.map +1 -1
  229. package/build/stores/apps.js +11 -0
  230. package/build/stores/apps.js.map +1 -1
  231. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  232. package/build/stores/bluetoothSettingKeys.js +2 -0
  233. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  234. package/build/stores/gallerySync.d.ts +2 -0
  235. package/build/stores/gallerySync.d.ts.map +1 -1
  236. package/build/stores/gallerySync.js +2 -0
  237. package/build/stores/gallerySync.js.map +1 -1
  238. package/build/stores/glasses.d.ts.map +1 -1
  239. package/build/stores/glasses.js +3 -0
  240. package/build/stores/glasses.js.map +1 -1
  241. package/build/stores/settings.d.ts +17 -0
  242. package/build/stores/settings.d.ts.map +1 -1
  243. package/build/stores/settings.js +152 -19
  244. package/build/stores/settings.js.map +1 -1
  245. package/build/types/applet.d.ts +2 -0
  246. package/build/types/applet.d.ts.map +1 -1
  247. package/build/types/applet.js.map +1 -1
  248. package/build/types/enums.d.ts +1 -0
  249. package/build/types/enums.d.ts.map +1 -1
  250. package/build/types/enums.js +1 -0
  251. package/build/types/enums.js.map +1 -1
  252. package/build/types/hardware.d.ts +1 -0
  253. package/build/types/hardware.d.ts.map +1 -1
  254. package/build/types/hardware.js +14 -0
  255. package/build/types/hardware.js.map +1 -1
  256. package/build/utils/configuredDevHost.d.ts +6 -0
  257. package/build/utils/configuredDevHost.d.ts.map +1 -0
  258. package/build/utils/configuredDevHost.js +19 -0
  259. package/build/utils/configuredDevHost.js.map +1 -0
  260. package/build/utils/devMiniappLaunch.d.ts +19 -8
  261. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  262. package/build/utils/devMiniappLaunch.js +144 -21
  263. package/build/utils/devMiniappLaunch.js.map +1 -1
  264. package/build/utils/diagnosticContext.d.ts.map +1 -1
  265. package/build/utils/diagnosticContext.js +15 -17
  266. package/build/utils/diagnosticContext.js.map +1 -1
  267. package/build/utils/hardware/hardware.d.ts.map +1 -1
  268. package/build/utils/hardware/hardware.js +7 -3
  269. package/build/utils/hardware/hardware.js.map +1 -1
  270. package/build/utils/miniappDiagnostics.d.ts +64 -0
  271. package/build/utils/miniappDiagnostics.d.ts.map +1 -0
  272. package/build/utils/miniappDiagnostics.js +173 -0
  273. package/build/utils/miniappDiagnostics.js.map +1 -0
  274. package/build/utils/timers/timers.d.ts.map +1 -1
  275. package/build/utils/timers/timers.js +55 -18
  276. package/build/utils/timers/timers.js.map +1 -1
  277. package/package.json +27 -18
  278. package/src/assets/silence.wav +0 -0
  279. package/src/engine.ts +7 -2
  280. package/src/facades/dev.ts +9 -5
  281. package/src/facades/gallery.ts +1 -1
  282. package/src/facades/glasses.ts +17 -4
  283. package/src/facades/glassesSettings.ts +5 -0
  284. package/src/facades/miniapps.ts +7 -0
  285. package/src/facades/ota.ts +19 -4
  286. package/src/facades/pairing.ts +11 -7
  287. package/src/facades/phoneNotifications.ts +4 -13
  288. package/src/index.ts +26 -2
  289. package/src/internal.ts +2 -1
  290. package/src/react/MentraLiveOtaFlow.tsx +856 -0
  291. package/src/react/index.ts +7 -0
  292. package/src/react/useEngineSnapshot.ts +16 -0
  293. package/src/runtime/bootstrap.ts +33 -1
  294. package/src/runtime/scanQrSeam.ts +20 -0
  295. package/src/services/AppRegistry.ts +72 -48
  296. package/src/services/AudioCloudUplink.ts +9 -3
  297. package/src/services/AudioPlaybackService.ts +94 -34
  298. package/src/services/BlobStore.ts +22 -9
  299. package/src/services/CloudAudioSubscriptionSync.ts +13 -1
  300. package/src/services/CloudClientService.ts +74 -0
  301. package/src/services/ForegroundLocationPermission.ts +24 -0
  302. package/src/services/GlassesSettingsSync.ts +9 -3
  303. package/src/services/GlassesStatusProjection.ts +16 -8
  304. package/src/services/HotspotOtaTransport.ts +184 -0
  305. package/src/services/HotspotShutdown.ts +41 -0
  306. package/src/services/LocalDisplayManager.ts +112 -21
  307. package/src/services/LocalMiniappRuntime.ts +576 -130
  308. package/src/services/LocalMiniappStorage.ts +82 -0
  309. package/src/services/LocalMiniappUserIdentity.ts +46 -0
  310. package/src/services/MicStateCoordinator.ts +169 -4
  311. package/src/services/MiniappLauncher.ts +30 -3
  312. package/src/services/MiniappLiveness.ts +24 -0
  313. package/src/services/OtaArtifactDownloader.ts +312 -0
  314. package/src/services/OtaAutoChain.ts +106 -0
  315. package/src/services/OtaErrorMapping.ts +80 -0
  316. package/src/services/OtaInstallCoordinator.ts +645 -143
  317. package/src/services/OtaService.ts +11 -5
  318. package/src/services/OtaUpdateCheckService.ts +180 -20
  319. package/src/services/PhoneCalendarService.ts +27 -4
  320. package/src/services/PhoneCameraFovCoordinator.ts +15 -0
  321. package/src/services/PhoneNotificationsSync.ts +12 -10
  322. package/src/services/PhonePhotoCoordinator.ts +33 -7
  323. package/src/services/PhoneStreamCoordinator.ts +139 -25
  324. package/src/services/PhoneVideoCoordinator.ts +11 -0
  325. package/src/services/SceneRenderer.ts +16 -0
  326. package/src/services/SupportProfileSync.ts +218 -0
  327. package/src/services/TtsTextSanitizer.ts +66 -0
  328. package/src/services/WifiAdbRequest.ts +53 -0
  329. package/src/services/asg/asgCameraApi.ts +99 -8
  330. package/src/services/asg/gallerySyncService.ts +28 -19
  331. package/src/services/asg/localNetworkTransport.ts +7 -11
  332. package/src/services/audioPlaybackAssets.ts +8 -0
  333. package/src/services/glassesClockSync.ts +2 -2
  334. package/src/services/manifestActions.ts +30 -0
  335. package/src/services/otaDisplayState.ts +57 -16
  336. package/src/services/otaInstallPolicy.ts +29 -3
  337. package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
  338. package/src/services/slimStreamStatus.ts +16 -5
  339. package/src/stores/apps.ts +12 -0
  340. package/src/stores/bluetoothSettingKeys.ts +2 -0
  341. package/src/stores/gallerySync.ts +4 -0
  342. package/src/stores/glasses.ts +3 -0
  343. package/src/stores/settings.ts +174 -21
  344. package/src/types/applet.ts +2 -0
  345. package/src/types/enums.ts +2 -1
  346. package/src/types/hardware.ts +17 -0
  347. package/src/utils/configuredDevHost.ts +17 -0
  348. package/src/utils/devMiniappLaunch.ts +151 -23
  349. package/src/utils/diagnosticContext.ts +17 -17
  350. package/src/utils/hardware/hardware.ts +7 -9
  351. package/src/utils/miniappDiagnostics.ts +226 -0
  352. package/src/utils/timers/timers.ts +57 -21
  353. package/build/services/asgOtaVersionUrl.d.ts +0 -2
  354. package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
  355. package/build/services/asgOtaVersionUrl.js.map +0 -1
@@ -8,7 +8,7 @@
8
8
  * This runtime handles request dispatch, stream fan-out, and lifecycle
9
9
  * management (connect/disconnect/ping).
10
10
  */
11
- import { Linking } from "react-native";
11
+ import { AppState, Linking } from "react-native";
12
12
  import Share from "react-native-share";
13
13
  import * as Battery from "expo-battery";
14
14
  import * as Clipboard from "expo-clipboard";
@@ -38,16 +38,24 @@ import { phoneStreamCoordinator } from "./PhoneStreamCoordinator";
38
38
  import { phoneVideoCoordinator } from "./PhoneVideoCoordinator";
39
39
  import { runSentenceTtsPipeline } from "./SentenceTtsPipeline";
40
40
  import { summarizeTranscriptionRoutes, transcriptionDeliveryRoute } from "./TranscriptionRouting";
41
+ import { prepareTtsSentences } from "./TtsTextSanitizer";
42
+ import { handleWifiAdbRequest } from "./WifiAdbRequest";
41
43
  import { cloudClientService } from "./CloudClientService";
42
44
  import { miniappLauncher } from "./MiniappLauncher";
43
45
  import { ISLAND_SETTINGS_KEYS, } from "../runtime/config";
44
46
  import { getAnalytics, getUiSeams } from "../runtime/bootstrap";
47
+ import { invokeScanQrSeam } from "../runtime/scanQrSeam";
45
48
  import { normalizeStreamAudioConfig, normalizeStreamVideoConfig } from "../runtime/streamConfig";
49
+ import { toLanguageHint } from "@mentra/cloud-protocol/languages";
50
+ import { buildMiniappManifestSnapshot } from "../utils/miniappDiagnostics";
46
51
  import ttsModelManager from "./TTSModelManager";
47
52
  import { NavigationHandlers } from "./NavigationHandlers";
48
53
  import { useAppStatusStore } from "../stores/apps";
49
54
  import { getDevAppAttestation, getDevAppSourcePackage } from "./AppRegistry";
55
+ import { resolveForegroundLocationPermission } from "./ForegroundLocationPermission";
56
+ import { advanceMiniappPingLiveness } from "./MiniappLiveness";
50
57
  import { listPhoneCalendarEvents, PhoneCalendarError } from "./PhoneCalendarService";
58
+ import { LocalMiniappStorage } from "./LocalMiniappStorage";
51
59
  /**
52
60
  * Strictness ordering for `location_stream` rate values. Higher index =
53
61
  * higher accuracy and higher power cost. The aggregate tier applied to
@@ -80,6 +88,14 @@ function withTimeout(promise, timeoutMs) {
80
88
  });
81
89
  }
82
90
  const LOG_TAG = "LOCAL_MINIAPP";
91
+ const DIAGNOSTIC_MAX_LIST_ITEMS = 100;
92
+ const DIAGNOSTIC_MAX_STRING_LENGTH = 512;
93
+ function diagnosticStringList(values) {
94
+ return [...values]
95
+ .map((value) => value.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH))
96
+ .sort()
97
+ .slice(0, DIAGNOSTIC_MAX_LIST_ITEMS);
98
+ }
83
99
  const SYSTEM_MINIAPP_PACKAGES = new Set([
84
100
  "com.mentra.camera",
85
101
  "com.mentra.gallery",
@@ -115,9 +131,27 @@ const RGB_LED_ACTIONS = new Set(["on", "off"]);
115
131
  const RGB_LED_COLORS = new Set(["red", "green", "blue", "orange", "white"]);
116
132
  const CAMERA_FOV_MIN = 62;
117
133
  const CAMERA_FOV_MAX = 118;
118
- const CAMERA_FOV_DEFAULT = 102;
119
- const CAMERA_FOV_PRESETS = { narrow: 82, standard: CAMERA_FOV_DEFAULT, wide: 118 };
134
+ const CAMERA_FOV_DEFAULT = 118;
135
+ const CAMERA_FOV_PRESETS = { narrow: 82, standard: 102, wide: 118 };
120
136
  const CAMERA_ROI_POSITION_BY_NAME = { center: "center", bottom: "bottom", top: "top" };
137
+ const localMiniappStorageBackend = {
138
+ get(key) {
139
+ const result = mmkvStorage.load(key);
140
+ return result.is_ok() ? result.value : null;
141
+ },
142
+ has(key) {
143
+ return mmkvStorage.load(key).is_ok();
144
+ },
145
+ set(key, value) {
146
+ mmkvStorage.save(key, value);
147
+ },
148
+ remove(key) {
149
+ mmkvStorage.remove(key);
150
+ },
151
+ keys() {
152
+ return mmkvStorage.getAllKeys();
153
+ },
154
+ };
121
155
  // =============================================================================
122
156
  // Declared-permission record helper (for CONNECT_ACK / PERMISSIONS_UPDATE)
123
157
  // =============================================================================
@@ -234,10 +268,19 @@ class LocalMiniappRuntime {
234
268
  connectWaiters = new Map();
235
269
  /** Ref-counted stream subscriptions: stream → set of packageNames. */
236
270
  streamSubscribers = new Map();
271
+ /** Host observers for the Mentra Live button-capture policy. */
272
+ buttonPressSubscriberListeners = new Set();
237
273
  /** Guards one-time wiring of the cloud transcript/translation fan-out. */
238
274
  cloudResultsWired = false;
239
275
  cloudStatusWired = false;
240
276
  cloudAudioSubscriptionSync = new CloudAudioSubscriptionSync();
277
+ /**
278
+ * Per-miniapp language hints from TRANSCRIPTION_CONFIG (issue 021 WP3).
279
+ * Bare ISO 639-1 codes, already registry-validated by the SDK and
280
+ * re-normalized here (defense in depth). Merged (union) into the auto-mode
281
+ * cloud transcription subscription in buildCloudAudioSubscriptions.
282
+ */
283
+ transcriptionHintsByApp = new Map();
241
284
  /** Ping interval handle. */
242
285
  pingIntervalId = null;
243
286
  foregroundProbeTimers = new Map();
@@ -259,6 +302,88 @@ class LocalMiniappRuntime {
259
302
  localSecret = `miniapp_${Date.now()}_${Math.random().toString(36).slice(2, 14)}`;
260
303
  usedTokens = new Set();
261
304
  constructor() { }
305
+ /** Snapshot of miniapps actively subscribed to hardware button events. */
306
+ getButtonPressSubscribers() {
307
+ return [...(this.streamSubscribers.get(MiniappStreamType.BUTTON_PRESS) ?? [])].sort();
308
+ }
309
+ /** Observe active hardware-button subscriptions. */
310
+ onButtonPressSubscribersChanged(listener) {
311
+ this.buttonPressSubscriberListeners.add(listener);
312
+ return () => this.buttonPressSubscriberListeners.delete(listener);
313
+ }
314
+ /** Report-safe live miniapp/subscription/resource snapshot. */
315
+ getDiagnosticSnapshot(nowMs = Date.now()) {
316
+ const connectedApps = [...this.connectedApps.entries()]
317
+ .sort(([a], [b]) => a.localeCompare(b))
318
+ .slice(0, DIAGNOSTIC_MAX_LIST_ITEMS)
319
+ .map(([packageName, app]) => ({
320
+ packageName: packageName.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH),
321
+ handshakeComplete: this.handshookApps.has(packageName),
322
+ subscriptions: diagnosticStringList(app.subscriptions),
323
+ lastMessageAgeMs: Math.max(0, nowMs - app.lastPongAt),
324
+ requestedLocationRate: app.requestedLocationRate,
325
+ hasActiveSpeakerStream: app.activeSpeakerStreamId !== undefined,
326
+ ...(app.installedManifest
327
+ ? {
328
+ manifest: buildMiniappManifestSnapshot(app.installedManifest, {
329
+ packageName,
330
+ name: app.installedManifest.name,
331
+ version: app.installedManifest.version,
332
+ type: app.installedManifest.type,
333
+ }),
334
+ }
335
+ : {}),
336
+ }));
337
+ const subscriptionsByStream = Object.fromEntries([...this.streamSubscribers.entries()]
338
+ .filter(([, packageNames]) => packageNames.size > 0)
339
+ .sort(([a], [b]) => a.localeCompare(b))
340
+ .slice(0, DIAGNOSTIC_MAX_LIST_ITEMS)
341
+ .map(([stream, packageNames]) => [
342
+ stream.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH),
343
+ diagnosticStringList(packageNames),
344
+ ]));
345
+ return {
346
+ connectedApps,
347
+ subscriptionsByStream,
348
+ resources: {
349
+ display: localDisplayManager.getDiagnosticSnapshot(),
350
+ camera: phonePhotoCoordinator.getDiagnosticSnapshot(),
351
+ video: phoneVideoCoordinator.getDiagnosticSnapshot(),
352
+ stream: phoneStreamCoordinator.getDiagnosticSnapshot(),
353
+ cameraFov: phoneCameraFovCoordinator.getDiagnosticSnapshot(),
354
+ },
355
+ };
356
+ }
357
+ replaceStreamSubscribers(packageName, previousStreams, nextStreams) {
358
+ const previousButtonSubscribers = this.getButtonPressSubscribers().join("\u0000");
359
+ for (const stream of previousStreams) {
360
+ const subscribers = this.streamSubscribers.get(stream);
361
+ if (!subscribers)
362
+ continue;
363
+ subscribers.delete(packageName);
364
+ if (subscribers.size === 0)
365
+ this.streamSubscribers.delete(stream);
366
+ }
367
+ for (const stream of nextStreams) {
368
+ let subscribers = this.streamSubscribers.get(stream);
369
+ if (!subscribers) {
370
+ subscribers = new Set();
371
+ this.streamSubscribers.set(stream, subscribers);
372
+ }
373
+ subscribers.add(packageName);
374
+ }
375
+ const nextButtonSubscribers = this.getButtonPressSubscribers();
376
+ if (previousButtonSubscribers !== nextButtonSubscribers.join("\u0000")) {
377
+ for (const listener of this.buttonPressSubscriberListeners) {
378
+ try {
379
+ listener(nextButtonSubscribers);
380
+ }
381
+ catch (error) {
382
+ console.warn(`${LOG_TAG}: button press subscriber listener threw:`, error);
383
+ }
384
+ }
385
+ }
386
+ }
262
387
  /**
263
388
  * Generate an HMAC-signed local session token for browser fallback auth.
264
389
  * Token format: base64(JSON({userId, packageName, exp})).base64(HMAC-SHA256(payload, secret))
@@ -466,14 +591,7 @@ class LocalMiniappRuntime {
466
591
  BgTimer.clearTimeout(existing.authRetryTimerId);
467
592
  existing.authRetryTimerId = null;
468
593
  }
469
- for (const stream of existing.subscriptions) {
470
- const subs = this.streamSubscribers.get(stream);
471
- if (subs) {
472
- subs.delete(packageName);
473
- if (subs.size === 0)
474
- this.streamSubscribers.delete(stream);
475
- }
476
- }
594
+ this.replaceStreamSubscribers(packageName, existing.subscriptions, []);
477
595
  this.recomputeMicRequirements();
478
596
  this.updateCloudSubscriptions();
479
597
  }
@@ -483,6 +601,7 @@ class LocalMiniappRuntime {
483
601
  cloudTranscriptionStreams: new Set(),
484
602
  sendMessage: sendFn,
485
603
  lastPongAt: Date.now(),
604
+ unansweredPingRounds: 0,
486
605
  installedManifest,
487
606
  authRefreshTimerId: null,
488
607
  authRetryTimerId: null,
@@ -602,6 +721,7 @@ class LocalMiniappRuntime {
602
721
  }
603
722
  unregisterApp(packageName) {
604
723
  console.log(`${LOG_TAG}: unregisterApp(${packageName})`);
724
+ const releasedMicGateOverride = micStateCoordinator.clearMiniappGateOverrides(packageName);
605
725
  this.clearForegroundProbe(packageName);
606
726
  this.clearMiniappAuthRefresh(packageName);
607
727
  this.clearMiniappAuthDeliveryRetry(packageName);
@@ -610,6 +730,9 @@ class LocalMiniappRuntime {
610
730
  // the early-return so it runs even if the connectedApps entry is already gone.
611
731
  this.handshookApps.delete(packageName);
612
732
  this.flushConnectWaiters(packageName, new Error(`${packageName} unregistered before connect`));
733
+ // Drop this app's transcription hints; the cloud subscription rebuild
734
+ // below (via replaceStreamSubscribers) re-derives the hint union without it.
735
+ this.transcriptionHintsByApp.delete(packageName);
613
736
  // Warm-up is a request-owned camera lease. Cancel it even if the app record was already
614
737
  // removed so a close racing the camera-open promise cannot leave the sensor running.
615
738
  // Keep camera teardown ordered: restoring FOV can restart the HAL, so the warm-up lease must
@@ -625,18 +748,16 @@ class LocalMiniappRuntime {
625
748
  console.warn(`${LOG_TAG}: failed to release camera FOV override for ${packageName} on unregister`, error);
626
749
  });
627
750
  const app = this.connectedApps.get(packageName);
628
- if (!app)
629
- return;
630
- // Remove from all stream subscriber sets
631
- for (const stream of app.subscriptions) {
632
- const subs = this.streamSubscribers.get(stream);
633
- if (subs) {
634
- subs.delete(packageName);
635
- if (subs.size === 0) {
636
- this.streamSubscribers.delete(stream);
637
- }
751
+ if (!app) {
752
+ if (releasedMicGateOverride) {
753
+ void micStateCoordinator.syncEffectiveGatePolicy(this.getConfiguredMicGates()).catch((error) => {
754
+ console.warn(`${LOG_TAG}: failed to restore mic gate policy for ${packageName}`, error);
755
+ });
638
756
  }
757
+ return;
639
758
  }
759
+ // Remove from all stream subscriber sets
760
+ this.replaceStreamSubscribers(packageName, app.subscriptions, []);
640
761
  // Reset per-session warning dedup so a relaunch surfaces issues again.
641
762
  resetPermissionWarnings(packageName);
642
763
  // Cancel lookahead/in-flight synthesis before stopping the current file so
@@ -691,6 +812,11 @@ class LocalMiniappRuntime {
691
812
  localDisplayManager.onUnmount(packageName);
692
813
  this.connectedApps.delete(packageName);
693
814
  this.recomputeMicRequirements();
815
+ if (releasedMicGateOverride) {
816
+ void micStateCoordinator.syncEffectiveGatePolicy(this.getConfiguredMicGates()).catch((error) => {
817
+ console.warn(`${LOG_TAG}: failed to restore mic gate policy for ${packageName}`, error);
818
+ });
819
+ }
694
820
  this.updateCloudSubscriptions();
695
821
  if (this.connectedApps.size === 0) {
696
822
  this.stopPingLoop();
@@ -856,9 +982,18 @@ class LocalMiniappRuntime {
856
982
  case MiniappRequestType.CAMERA_FOV:
857
983
  void this.handleCameraFov(packageName, payload, requestId);
858
984
  break;
985
+ case MiniappRequestType.SET_WIFI_ADB_STATE:
986
+ void this.handleSetWifiAdbState(packageName, payload, requestId);
987
+ break;
859
988
  case MiniappRequestType.IMU_SET_ENABLED:
860
989
  this.handleImuSetEnabled(packageName, payload, requestId);
861
990
  break;
991
+ case MiniappRequestType.MIC_SET_VAD_ENABLED:
992
+ void this.handleMicSetVadEnabled(packageName, payload, requestId);
993
+ break;
994
+ case MiniappRequestType.MIC_SET_LOUDNESS_GATE_ENABLED:
995
+ void this.handleMicSetLoudnessGateEnabled(packageName, payload, requestId);
996
+ break;
862
997
  case MiniappRequestType.PING:
863
998
  // SDK should handle this itself; reply PONG just in case
864
999
  this.sendToMiniapp(packageName, { type: MiniappResponseType.PONG }, requestId);
@@ -880,51 +1015,54 @@ class LocalMiniappRuntime {
880
1015
  case MiniappRequestType.DOWNLOAD:
881
1016
  this.handleDownload(packageName, payload, requestId);
882
1017
  break;
1018
+ case MiniappRequestType.SCAN_QR:
1019
+ void this.handleScanQr(packageName, payload, requestId);
1020
+ break;
883
1021
  // Persistent binary blob storage (session.blob)
884
1022
  case MiniappRequestType.BLOB_CREATE:
885
- this.blobStore.handleCreate(packageName, payload, requestId);
1023
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleCreate(packageName, payload, requestId));
886
1024
  break;
887
1025
  case MiniappRequestType.BLOB_WRITE:
888
- this.blobStore.handleWrite(packageName, payload, requestId);
1026
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleWrite(packageName, payload, requestId));
889
1027
  break;
890
1028
  case MiniappRequestType.BLOB_COMMIT:
891
- this.blobStore.handleCommit(packageName, payload, requestId);
1029
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleCommit(packageName, payload, requestId));
892
1030
  break;
893
1031
  case MiniappRequestType.BLOB_ABORT:
894
- this.blobStore.handleAbort(packageName, payload, requestId);
1032
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleAbort(packageName, payload, requestId));
895
1033
  break;
896
1034
  case MiniappRequestType.BLOB_SET_FROM_URL:
897
- void this.blobStore.handleSetFromUrl(packageName, payload, requestId);
1035
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleSetFromUrl(packageName, payload, requestId));
898
1036
  break;
899
1037
  case MiniappRequestType.BLOB_IMPORT:
900
- void this.blobStore.handleImport(packageName, payload, requestId);
1038
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleImport(packageName, payload, requestId));
901
1039
  break;
902
1040
  case MiniappRequestType.BLOB_GET:
903
- this.blobStore.handleGet(packageName, payload, requestId);
1041
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleGet(packageName, payload, requestId));
904
1042
  break;
905
1043
  case MiniappRequestType.BLOB_LIST:
906
- this.blobStore.handleList(packageName, payload, requestId);
1044
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleList(packageName, payload, requestId));
907
1045
  break;
908
1046
  case MiniappRequestType.BLOB_USAGE:
909
- this.blobStore.handleUsage(packageName, payload, requestId);
1047
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleUsage(packageName, payload, requestId));
910
1048
  break;
911
1049
  case MiniappRequestType.BLOB_DELETE:
912
- this.blobStore.handleDelete(packageName, payload, requestId);
1050
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleDelete(packageName, payload, requestId));
913
1051
  break;
914
1052
  case MiniappRequestType.BLOB_CLEAR:
915
- this.blobStore.handleClear(packageName, payload, requestId);
1053
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleClear(packageName, payload, requestId));
916
1054
  break;
917
1055
  case MiniappRequestType.BLOB_OPEN_READ:
918
- this.blobStore.handleOpenRead(packageName, payload, requestId);
1056
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleOpenRead(packageName, payload, requestId));
919
1057
  break;
920
1058
  case MiniappRequestType.BLOB_READ:
921
- this.blobStore.handleRead(packageName, payload, requestId);
1059
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleRead(packageName, payload, requestId));
922
1060
  break;
923
1061
  case MiniappRequestType.BLOB_CLOSE_READ:
924
- this.blobStore.handleCloseRead(packageName, payload, requestId);
1062
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleCloseRead(packageName, payload, requestId));
925
1063
  break;
926
1064
  case MiniappRequestType.BLOB_SHARE:
927
- void this.blobStore.handleShare(packageName, payload, requestId);
1065
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleShare(packageName, payload, requestId));
928
1066
  break;
929
1067
  // Cloud-coordinated features
930
1068
  case MiniappRequestType.PHOTO:
@@ -977,8 +1115,19 @@ class LocalMiniappRuntime {
977
1115
  message: "Dashboard API is deferred in v1",
978
1116
  });
979
1117
  break;
1118
+ case MiniappRequestType.TRANSCRIPTION_CONFIG:
1119
+ this.handleTranscriptionConfig(packageName, payload, requestId);
1120
+ break;
980
1121
  default:
1122
+ // NACK instead of silent drop (issue 021 S3): an SDK that sends a
1123
+ // request this runtime doesn't implement must get a rejected promise,
1124
+ // not a permanently-pending silent gap. One-shots (no requestId) can
1125
+ // only be logged.
981
1126
  console.warn(`${LOG_TAG}: Unknown request type from ${packageName}: ${requestType}`);
1127
+ this.sendResult(packageName, requestId, false, undefined, {
1128
+ code: MiniappErrorCode.NOT_IMPLEMENTED,
1129
+ message: `Unknown or unimplemented request type: ${requestType}`,
1130
+ });
982
1131
  break;
983
1132
  }
984
1133
  }
@@ -995,6 +1144,7 @@ class LocalMiniappRuntime {
995
1144
  }
996
1145
  // Update lastPongAt so it doesn't time out right away
997
1146
  existing.lastPongAt = Date.now();
1147
+ existing.unansweredPingRounds = 0;
998
1148
  // Read current glasses capabilities from the settings store
999
1149
  const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable);
1000
1150
  const capabilities = getModelCapabilities(defaultWearable || DeviceTypes.NONE);
@@ -1182,6 +1332,51 @@ class LocalMiniappRuntime {
1182
1332
  this.deliverInitialMiniappAuth(packageName, undefined, 0);
1183
1333
  }
1184
1334
  }
1335
+ /**
1336
+ * TRANSCRIPTION_CONFIG (issue 021 WP3): store the miniapp's language hints
1337
+ * and rebuild the cloud subscription set so the auto-mode subscription
1338
+ * carries them. Replies with a REQUEST_RESULT so the SDK's configure()
1339
+ * promise settles: unknown hint values are an error, not a silent drop.
1340
+ */
1341
+ handleTranscriptionConfig(packageName, payload, requestId) {
1342
+ const app = this.connectedApps.get(packageName);
1343
+ if (!app)
1344
+ return;
1345
+ const config = (payload.config ?? {});
1346
+ const rawHints = config.languageHints;
1347
+ if (rawHints !== undefined && !Array.isArray(rawHints)) {
1348
+ this.sendResult(packageName, requestId, false, undefined, {
1349
+ code: MiniappErrorCode.INTERNAL,
1350
+ message: "languageHints must be an array of language codes",
1351
+ });
1352
+ return;
1353
+ }
1354
+ if (rawHints === undefined || rawHints.length === 0) {
1355
+ this.transcriptionHintsByApp.delete(packageName);
1356
+ }
1357
+ else {
1358
+ // Defense in depth: the SDK already validated, but the wire is untyped.
1359
+ const hints = [];
1360
+ const invalid = [];
1361
+ for (const raw of rawHints) {
1362
+ const hint = typeof raw === "string" ? toLanguageHint(raw) : null;
1363
+ if (hint)
1364
+ hints.push(hint);
1365
+ else
1366
+ invalid.push(String(raw));
1367
+ }
1368
+ if (invalid.length > 0) {
1369
+ this.sendResult(packageName, requestId, false, undefined, {
1370
+ code: MiniappErrorCode.INTERNAL,
1371
+ message: `Unknown language hint(s): ${invalid.join(", ")}`,
1372
+ });
1373
+ return;
1374
+ }
1375
+ this.transcriptionHintsByApp.set(packageName, [...new Set(hints)].sort());
1376
+ }
1377
+ this.updateCloudSubscriptions();
1378
+ this.sendResult(packageName, requestId, true, { applied: true });
1379
+ }
1185
1380
  handleSubscribe(packageName, payload, requestId) {
1186
1381
  const app = this.connectedApps.get(packageName);
1187
1382
  if (!app)
@@ -1254,28 +1449,12 @@ class LocalMiniappRuntime {
1254
1449
  return;
1255
1450
  }
1256
1451
  }
1257
- // Remove old subscriptions for this app
1258
- for (const oldStream of app.subscriptions) {
1259
- const subs = this.streamSubscribers.get(oldStream);
1260
- if (subs) {
1261
- subs.delete(packageName);
1262
- if (subs.size === 0) {
1263
- this.streamSubscribers.delete(oldStream);
1264
- }
1265
- }
1266
- }
1267
1452
  // Add new subscriptions
1453
+ const previousSubscriptions = app.subscriptions;
1268
1454
  app.subscriptions = new Set(streams);
1269
1455
  app.forceLocalTranscriptionStreams = new Set([...forceLocalTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
1270
1456
  app.cloudTranscriptionStreams = new Set([...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
1271
- for (const stream of streams) {
1272
- let subs = this.streamSubscribers.get(stream);
1273
- if (!subs) {
1274
- subs = new Set();
1275
- this.streamSubscribers.set(stream, subs);
1276
- }
1277
- subs.add(packageName);
1278
- }
1457
+ this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions);
1279
1458
  this.recomputeMicRequirements();
1280
1459
  this.updateCloudSubscriptions();
1281
1460
  this.recomputeHeadingSubscription();
@@ -1718,15 +1897,20 @@ class LocalMiniappRuntime {
1718
1897
  }
1719
1898
  async handleSpeak(packageName, payload, requestId) {
1720
1899
  const rawText = payload.text;
1721
- const sentences = typeof rawText === "string"
1900
+ const rawSentences = typeof rawText === "string"
1722
1901
  ? [rawText.trim()].filter(Boolean)
1723
1902
  : Array.isArray(rawText)
1724
- ? rawText.filter((sentence) => typeof sentence === "string").map((sentence) => sentence.trim()).filter(Boolean)
1903
+ ? rawText
1904
+ .filter((sentence) => typeof sentence === "string")
1905
+ .map((sentence) => sentence.trim())
1906
+ .filter(Boolean)
1725
1907
  : [];
1908
+ const enableSanitization = payload.enableSanitization !== false;
1909
+ const sentences = prepareTtsSentences(rawSentences, enableSanitization);
1726
1910
  if (sentences.length === 0) {
1727
1911
  this.sendResult(packageName, requestId, false, undefined, {
1728
1912
  code: MiniappErrorCode.INTERNAL,
1729
- message: "speak requires text or a non-empty sentence list",
1913
+ message: "speak requires text or a non-empty sentence list with speakable content",
1730
1914
  });
1731
1915
  return;
1732
1916
  }
@@ -1819,7 +2003,6 @@ class LocalMiniappRuntime {
1819
2003
  appId: packageName,
1820
2004
  volume,
1821
2005
  stopOtherAudio,
1822
- suppressCloudUplink: true,
1823
2006
  }, (_responseId, success, error, duration, completionReason) => {
1824
2007
  if (run.playbackRequestId === sentenceRequestId)
1825
2008
  run.playbackRequestId = undefined;
@@ -1863,7 +2046,6 @@ class LocalMiniappRuntime {
1863
2046
  appId: packageName,
1864
2047
  volume,
1865
2048
  stopOtherAudio,
1866
- suppressCloudUplink: true,
1867
2049
  }, (_respId, success, error, duration, completionReason) => {
1868
2050
  if (run.playbackRequestId === audioRequestId)
1869
2051
  run.playbackRequestId = undefined;
@@ -1909,7 +2091,6 @@ class LocalMiniappRuntime {
1909
2091
  appId: packageName,
1910
2092
  volume,
1911
2093
  stopOtherAudio,
1912
- suppressCloudUplink: true,
1913
2094
  }, (_respId, success, error, duration, completionReason) => {
1914
2095
  if (run.playbackRequestId === audioRequestId)
1915
2096
  run.playbackRequestId = undefined;
@@ -1990,7 +2171,15 @@ class LocalMiniappRuntime {
1990
2171
  }
1991
2172
  async handleLocationPoll(packageName, requestId) {
1992
2173
  try {
1993
- const { status } = await Location.requestForegroundPermissionsAsync();
2174
+ // Expo's Android requestForegroundPermissionsAsync always delegates to
2175
+ // Activity.requestPermissions(), even when permission is already granted.
2176
+ // Calling it after the user presses Home therefore waits until the Activity
2177
+ // resumes. Read the current grant first, and only enter the prompt flow
2178
+ // while the Activity is actually foregrounded.
2179
+ const permissionStartedAt = Date.now();
2180
+ const permission = await resolveForegroundLocationPermission(Location, () => AppState.currentState);
2181
+ const { status } = permission;
2182
+ console.log(`${LOG_TAG}: location poll permission status=${status} appState=${AppState.currentState} elapsed=${Date.now() - permissionStartedAt}ms`);
1994
2183
  if (status !== "granted") {
1995
2184
  this.sendResult(packageName, requestId, false, undefined, {
1996
2185
  code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
@@ -2002,8 +2191,11 @@ class LocalMiniappRuntime {
2002
2191
  // a fresh fix if the cache is empty. Use Low accuracy on the fresh
2003
2192
  // path so we get a cell/wifi-tower fix in ~1s instead of waiting
2004
2193
  // for full GPS warm-up.
2194
+ const locationStartedAt = Date.now();
2005
2195
  const cached = await Location.getLastKnownPositionAsync({ maxAge: 60_000 });
2196
+ console.log(`${LOG_TAG}: location poll cache hit=${cached !== null} elapsed=${Date.now() - locationStartedAt}ms`);
2006
2197
  const location = cached ?? (await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Low }));
2198
+ console.log(`${LOG_TAG}: location poll resolved elapsed=${Date.now() - locationStartedAt}ms`);
2007
2199
  this.sendResult(packageName, requestId, true, {
2008
2200
  lat: location.coords.latitude,
2009
2201
  lng: location.coords.longitude,
@@ -2038,8 +2230,64 @@ class LocalMiniappRuntime {
2038
2230
  */
2039
2231
  blobStore = new BlobStore({
2040
2232
  sendResult: (packageName, requestId, ok, result, error) => this.sendResult(packageName, requestId, ok, result, error),
2041
- getUserId: () => useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.coreToken) || "anonymous",
2233
+ getUserId: () => cloudClientService.getMentraUserId(),
2234
+ });
2235
+ blobRequestQueues = new Map();
2236
+ simpleStorage = new LocalMiniappStorage({
2237
+ backend: localMiniappStorageBackend,
2238
+ getUserId: () => cloudClientService.resolveMentraUserId(),
2042
2239
  });
2240
+ /**
2241
+ * BlobStore's file helpers are synchronous once a user namespace is selected,
2242
+ * but first-boot Core auth is not. Queue requests per app behind stable
2243
+ * identity resolution so CREATE/WRITE/COMMIT ordering survives that wait.
2244
+ */
2245
+ enqueueBlobRequest(packageName, requestId, operation) {
2246
+ const app = this.connectedApps.get(packageName);
2247
+ const previous = this.blobRequestQueues.get(packageName) ?? Promise.resolve();
2248
+ const queued = previous.then(async () => {
2249
+ try {
2250
+ await cloudClientService.resolveMentraUserId();
2251
+ }
2252
+ catch (err) {
2253
+ console.error(`${LOG_TAG}: blob identity error:`, err);
2254
+ this.sendResult(packageName, requestId, false, undefined, {
2255
+ code: MiniappErrorCode.NOT_CONNECTED,
2256
+ message: "Blob storage is unavailable before sign-in completes",
2257
+ });
2258
+ return;
2259
+ }
2260
+ // unregisterApp() or a crash-respawn may have replaced this app while
2261
+ // identity resolution was pending. Never replay an old context's request
2262
+ // against its replacement.
2263
+ if (!app || this.connectedApps.get(packageName) !== app)
2264
+ return;
2265
+ // Invoke in request order, but do not keep the queue occupied for the
2266
+ // lifetime of an async picker, download, or share sheet. CREATE/WRITE/
2267
+ // COMMIT are synchronous and therefore still dispatch strictly in order.
2268
+ try {
2269
+ const pending = operation();
2270
+ if (pending) {
2271
+ void pending.catch((err) => this.handleBlobRequestError(packageName, requestId, err));
2272
+ }
2273
+ }
2274
+ catch (err) {
2275
+ this.handleBlobRequestError(packageName, requestId, err);
2276
+ }
2277
+ });
2278
+ this.blobRequestQueues.set(packageName, queued);
2279
+ void queued.finally(() => {
2280
+ if (this.blobRequestQueues.get(packageName) === queued)
2281
+ this.blobRequestQueues.delete(packageName);
2282
+ });
2283
+ }
2284
+ handleBlobRequestError(packageName, requestId, err) {
2285
+ console.error(`${LOG_TAG}: blob request error:`, err);
2286
+ this.sendResult(packageName, requestId, false, undefined, {
2287
+ code: MiniappErrorCode.INTERNAL,
2288
+ message: err instanceof Error ? err.message : "Blob storage error",
2289
+ });
2290
+ }
2043
2291
  /**
2044
2292
  * Heading is a sensor stream — start the native compass when any mini
2045
2293
  * app is subscribed to "heading_update", stop it when none are.
@@ -2114,10 +2362,6 @@ class LocalMiniappRuntime {
2114
2362
  // ---------------------------------------------------------------------------
2115
2363
  // Storage helpers
2116
2364
  // ---------------------------------------------------------------------------
2117
- getStorageKeyPrefix(packageName) {
2118
- const userId = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.coreToken) || "anonymous";
2119
- return `mentraos_localstorage_${userId}_${packageName}_`;
2120
- }
2121
2365
  async handleStorageGet(packageName, payload, requestId) {
2122
2366
  try {
2123
2367
  const key = payload.key;
@@ -2128,9 +2372,8 @@ class LocalMiniappRuntime {
2128
2372
  });
2129
2373
  return;
2130
2374
  }
2131
- const fullKey = this.getStorageKeyPrefix(packageName) + key;
2132
- const result = mmkvStorage.load(fullKey);
2133
- this.sendResult(packageName, requestId, true, { key, value: result.is_ok() ? result.value : null });
2375
+ const value = await this.simpleStorage.get(packageName, key);
2376
+ this.sendResult(packageName, requestId, true, { key, value });
2134
2377
  }
2135
2378
  catch (err) {
2136
2379
  console.error(`${LOG_TAG}: storage_get error:`, err);
@@ -2150,8 +2393,7 @@ class LocalMiniappRuntime {
2150
2393
  });
2151
2394
  return;
2152
2395
  }
2153
- const fullKey = this.getStorageKeyPrefix(packageName) + key;
2154
- mmkvStorage.save(fullKey, payload.value ?? null);
2396
+ await this.simpleStorage.set(packageName, key, payload.value ?? null);
2155
2397
  this.sendResult(packageName, requestId, true);
2156
2398
  }
2157
2399
  catch (err) {
@@ -2172,8 +2414,7 @@ class LocalMiniappRuntime {
2172
2414
  });
2173
2415
  return;
2174
2416
  }
2175
- const fullKey = this.getStorageKeyPrefix(packageName) + key;
2176
- mmkvStorage.remove(fullKey);
2417
+ await this.simpleStorage.delete(packageName, key);
2177
2418
  this.sendResult(packageName, requestId, true);
2178
2419
  }
2179
2420
  catch (err) {
@@ -2186,9 +2427,7 @@ class LocalMiniappRuntime {
2186
2427
  }
2187
2428
  async handleStorageList(packageName, _payload, requestId) {
2188
2429
  try {
2189
- const prefix = this.getStorageKeyPrefix(packageName);
2190
- const allKeys = mmkvStorage.getAllKeys();
2191
- const keys = allKeys.filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
2430
+ const keys = await this.simpleStorage.keys(packageName);
2192
2431
  this.sendResult(packageName, requestId, true, { keys });
2193
2432
  }
2194
2433
  catch (err) {
@@ -2201,12 +2440,7 @@ class LocalMiniappRuntime {
2201
2440
  }
2202
2441
  async handleStorageClear(packageName, requestId) {
2203
2442
  try {
2204
- const prefix = this.getStorageKeyPrefix(packageName);
2205
- const allKeys = mmkvStorage.getAllKeys();
2206
- for (const k of allKeys) {
2207
- if (k.startsWith(prefix))
2208
- mmkvStorage.remove(k);
2209
- }
2443
+ await this.simpleStorage.clear(packageName);
2210
2444
  this.sendResult(packageName, requestId, true);
2211
2445
  }
2212
2446
  catch (err) {
@@ -2227,9 +2461,8 @@ class LocalMiniappRuntime {
2227
2461
  });
2228
2462
  return;
2229
2463
  }
2230
- const fullKey = this.getStorageKeyPrefix(packageName) + key;
2231
- const result = mmkvStorage.load(fullKey);
2232
- this.sendResult(packageName, requestId, true, { has: result.is_ok() });
2464
+ const has = await this.simpleStorage.has(packageName, key);
2465
+ this.sendResult(packageName, requestId, true, { has });
2233
2466
  }
2234
2467
  catch (err) {
2235
2468
  console.error(`${LOG_TAG}: storage_has error:`, err);
@@ -2241,18 +2474,7 @@ class LocalMiniappRuntime {
2241
2474
  }
2242
2475
  async handleStorageGetAll(packageName, requestId) {
2243
2476
  try {
2244
- const prefix = this.getStorageKeyPrefix(packageName);
2245
- const allKeys = mmkvStorage.getAllKeys();
2246
- const values = {};
2247
- for (const k of allKeys) {
2248
- if (!k.startsWith(prefix))
2249
- continue;
2250
- const r = mmkvStorage.load(k);
2251
- if (r.is_ok()) {
2252
- const v = r.value;
2253
- values[k.slice(prefix.length)] = typeof v === "string" ? v : String(v ?? "");
2254
- }
2255
- }
2477
+ const values = await this.simpleStorage.getAll(packageName);
2256
2478
  this.sendResult(packageName, requestId, true, { values });
2257
2479
  }
2258
2480
  catch (err) {
@@ -2273,10 +2495,7 @@ class LocalMiniappRuntime {
2273
2495
  });
2274
2496
  return;
2275
2497
  }
2276
- const prefix = this.getStorageKeyPrefix(packageName);
2277
- for (const [key, value] of Object.entries(values)) {
2278
- mmkvStorage.save(prefix + key, value ?? null);
2279
- }
2498
+ await this.simpleStorage.setMultiple(packageName, values);
2280
2499
  this.sendResult(packageName, requestId, true);
2281
2500
  }
2282
2501
  catch (err) {
@@ -2288,6 +2507,29 @@ class LocalMiniappRuntime {
2288
2507
  }
2289
2508
  }
2290
2509
  // ---------------------------------------------------------------------------
2510
+ // Wi-Fi ADB
2511
+ // ---------------------------------------------------------------------------
2512
+ async handleSetWifiAdbState(packageName, payload, requestId) {
2513
+ const result = await handleWifiAdbRequest(packageName, payload, {
2514
+ isSystemPackage: (candidate) => this.isSystemPackage(candidate),
2515
+ setWifiAdbState: async (enabled) => {
2516
+ console.log(`${LOG_TAG}: set_wifi_adb_state enabled=${enabled} from ${packageName}`);
2517
+ try {
2518
+ await BluetoothSdk.setWifiAdbState(enabled);
2519
+ }
2520
+ catch (error) {
2521
+ console.error(`${LOG_TAG}: set_wifi_adb_state error:`, error);
2522
+ throw error;
2523
+ }
2524
+ },
2525
+ });
2526
+ if (result.ok) {
2527
+ this.sendResult(packageName, requestId, true);
2528
+ return;
2529
+ }
2530
+ this.sendResult(packageName, requestId, false, undefined, result.error);
2531
+ }
2532
+ // ---------------------------------------------------------------------------
2291
2533
  // Camera FOV
2292
2534
  // ---------------------------------------------------------------------------
2293
2535
  async handleCameraFov(packageName, payload, requestId) {
@@ -2344,6 +2586,103 @@ class LocalMiniappRuntime {
2344
2586
  });
2345
2587
  }
2346
2588
  }
2589
+ /**
2590
+ * Lifecycle-scoped glasses-side VAD override from a miniapp
2591
+ * (session.mic.setVoiceActivityDetectionEnabled). Requires MICROPHONE in the
2592
+ * miniapp manifest. Mentra Live only today — other models no-op via DeviceStore.
2593
+ */
2594
+ async handleMicSetVadEnabled(packageName, payload, requestId) {
2595
+ const app = this.connectedApps.get(packageName);
2596
+ const hasMicPermission = app?.installedManifest?.permissions?.some((p) => p.type === "MICROPHONE");
2597
+ if (!hasMicPermission) {
2598
+ logPermissionNotDeclared(packageName, "MICROPHONE", "to set VAD enabled", `{"type": "MICROPHONE"}`);
2599
+ this.sendResult(packageName, requestId, false, undefined, {
2600
+ code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
2601
+ message: `MICROPHONE permission not declared in miniapp.json. Add {"type": "MICROPHONE"} to the "permissions" array.`,
2602
+ permission: "MICROPHONE",
2603
+ operation: MiniappRequestType.MIC_SET_VAD_ENABLED,
2604
+ });
2605
+ return;
2606
+ }
2607
+ if (typeof payload.enabled !== "boolean") {
2608
+ this.sendResult(packageName, requestId, false, undefined, {
2609
+ code: MiniappErrorCode.INVALID_ARGUMENT,
2610
+ message: "enabled must be a boolean",
2611
+ });
2612
+ return;
2613
+ }
2614
+ try {
2615
+ const enabled = payload.enabled;
2616
+ console.log(`${LOG_TAG}: mic_set_vad_enabled ${enabled} (by ${packageName})`);
2617
+ await micStateCoordinator.setMiniappGateOverride(packageName, "vad", enabled, this.getConfiguredMicGates());
2618
+ this.sendResult(packageName, requestId, true);
2619
+ }
2620
+ catch (err) {
2621
+ console.error(`${LOG_TAG}: mic_set_vad_enabled error:`, err);
2622
+ this.sendResult(packageName, requestId, false, undefined, {
2623
+ code: MiniappErrorCode.INTERNAL,
2624
+ message: err instanceof Error ? err.message : "VAD set-enabled error",
2625
+ });
2626
+ }
2627
+ }
2628
+ /**
2629
+ * Lifecycle-scoped center-mic loudness gate ("Barrier") override from a miniapp
2630
+ * (session.mic.setLoudnessGateEnabled). Requires MICROPHONE in the miniapp
2631
+ * manifest. Mentra Live only today — other models no-op via DeviceStore.
2632
+ */
2633
+ async handleMicSetLoudnessGateEnabled(packageName, payload, requestId) {
2634
+ const app = this.connectedApps.get(packageName);
2635
+ const hasMicPermission = app?.installedManifest?.permissions?.some((p) => p.type === "MICROPHONE");
2636
+ if (!hasMicPermission) {
2637
+ logPermissionNotDeclared(packageName, "MICROPHONE", "to set loudness gate enabled", `{"type": "MICROPHONE"}`);
2638
+ this.sendResult(packageName, requestId, false, undefined, {
2639
+ code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
2640
+ message: `MICROPHONE permission not declared in miniapp.json. Add {"type": "MICROPHONE"} to the "permissions" array.`,
2641
+ permission: "MICROPHONE",
2642
+ operation: MiniappRequestType.MIC_SET_LOUDNESS_GATE_ENABLED,
2643
+ });
2644
+ return;
2645
+ }
2646
+ if (typeof payload.enabled !== "boolean") {
2647
+ this.sendResult(packageName, requestId, false, undefined, {
2648
+ code: MiniappErrorCode.INVALID_ARGUMENT,
2649
+ message: "enabled must be a boolean",
2650
+ });
2651
+ return;
2652
+ }
2653
+ try {
2654
+ const enabled = payload.enabled;
2655
+ console.log(`${LOG_TAG}: mic_set_loudness_gate_enabled ${enabled} (by ${packageName})`);
2656
+ await micStateCoordinator.setMiniappGateOverride(packageName, "loudnessGate", enabled, this.getConfiguredMicGates());
2657
+ this.sendResult(packageName, requestId, true);
2658
+ }
2659
+ catch (err) {
2660
+ console.error(`${LOG_TAG}: mic_set_loudness_gate_enabled error:`, err);
2661
+ this.sendResult(packageName, requestId, false, undefined, {
2662
+ code: MiniappErrorCode.INTERNAL,
2663
+ message: err instanceof Error ? err.message : "Loudness gate set-enabled error",
2664
+ });
2665
+ }
2666
+ }
2667
+ /**
2668
+ * session.system.scanQr — host camera overlay. Must not clear miniapp
2669
+ * foreground; the host seam is responsible for presenting a Modal on top.
2670
+ */
2671
+ async handleScanQr(packageName, payload, requestId) {
2672
+ try {
2673
+ const result = await invokeScanQrSeam(getUiSeams().scanQr, payload);
2674
+ this.sendResult(packageName, requestId, true, result);
2675
+ }
2676
+ catch (err) {
2677
+ if (!getUiSeams().scanQr) {
2678
+ console.warn(`${LOG_TAG}: scanQr seam missing — QR overlay is not registered`);
2679
+ }
2680
+ this.sendResult(packageName, requestId, false, undefined, {
2681
+ code: err?.code || MiniappErrorCode.INTERNAL,
2682
+ message: err instanceof Error ? err.message : "QR scan failed",
2683
+ });
2684
+ }
2685
+ }
2347
2686
  // ---------------------------------------------------------------------------
2348
2687
  // System utilities (share, open URL, clipboard, download)
2349
2688
  // ---------------------------------------------------------------------------
@@ -2481,8 +2820,8 @@ class LocalMiniappRuntime {
2481
2820
  isoCap: payload.isoCap,
2482
2821
  noiseReduction: payload.noiseReduction,
2483
2822
  edgeEnhancement: payload.edgeEnhancement,
2484
- mfnr: payload.mfnr,
2485
2823
  zsl: payload.zsl,
2824
+ mfnr: payload.mfnr,
2486
2825
  ispDigitalGain: payload.ispDigitalGain,
2487
2826
  ispAnalogGain: payload.ispAnalogGain,
2488
2827
  });
@@ -2517,6 +2856,8 @@ class LocalMiniappRuntime {
2517
2856
  mode: payload.mode,
2518
2857
  exposureTimeNs: payload.exposureTimeNs,
2519
2858
  durationMs: payload.durationMs,
2859
+ zsl: payload.zsl,
2860
+ mfnr: payload.mfnr,
2520
2861
  });
2521
2862
  this.sendResult(packageName, requestId, true);
2522
2863
  }
@@ -2596,6 +2937,7 @@ class LocalMiniappRuntime {
2596
2937
  video: normalizeStreamVideoConfig(payload.video),
2597
2938
  audio: normalizeStreamAudioConfig(payload.audio),
2598
2939
  sound: payload.sound,
2940
+ authToken: typeof payload.authToken === "string" ? payload.authToken : undefined,
2599
2941
  });
2600
2942
  this.sendResult(packageName, requestId, true, result);
2601
2943
  }
@@ -2674,7 +3016,7 @@ class LocalMiniappRuntime {
2674
3016
  return;
2675
3017
  }
2676
3018
  try {
2677
- await requestWifiSetup(payload.reason);
3019
+ await requestWifiSetup(payload.reason, packageName);
2678
3020
  this.sendResult(packageName, requestId, true);
2679
3021
  }
2680
3022
  catch (err) {
@@ -2733,28 +3075,12 @@ class LocalMiniappRuntime {
2733
3075
  return { ok: false, error: "MICROPHONE permission not declared in miniapp.json" };
2734
3076
  }
2735
3077
  }
2736
- // Remove old subscriptions for this app
2737
- for (const oldStream of app.subscriptions) {
2738
- const subs = this.streamSubscribers.get(oldStream);
2739
- if (subs) {
2740
- subs.delete(packageName);
2741
- if (subs.size === 0) {
2742
- this.streamSubscribers.delete(oldStream);
2743
- }
2744
- }
2745
- }
2746
3078
  // Add new subscriptions
3079
+ const previousSubscriptions = app.subscriptions;
2747
3080
  app.subscriptions = new Set(streams);
2748
3081
  app.forceLocalTranscriptionStreams.clear();
2749
3082
  app.cloudTranscriptionStreams = new Set(streams.filter((stream) => stream.startsWith("transcription:")));
2750
- for (const stream of streams) {
2751
- let subs = this.streamSubscribers.get(stream);
2752
- if (!subs) {
2753
- subs = new Set();
2754
- this.streamSubscribers.set(stream, subs);
2755
- }
2756
- subs.add(packageName);
2757
- }
3083
+ this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions);
2758
3084
  this.recomputeMicRequirements();
2759
3085
  this.updateCloudSubscriptions();
2760
3086
  return { ok: true };
@@ -2762,6 +3088,15 @@ class LocalMiniappRuntime {
2762
3088
  // ===========================================================================
2763
3089
  // Mic requirements
2764
3090
  // ===========================================================================
3091
+ getConfiguredMicGates() {
3092
+ const bluetoothSettings = useSettingsStore.getState().getBluetoothSettings();
3093
+ const configuredVad = bluetoothSettings.voice_activity_detection_enabled;
3094
+ const configuredLoudnessGate = bluetoothSettings.loudness_gate_enabled;
3095
+ return {
3096
+ vadEnabled: typeof configuredVad === "boolean" ? configuredVad : null,
3097
+ loudnessGateEnabled: typeof configuredLoudnessGate === "boolean" ? configuredLoudnessGate : null,
3098
+ };
3099
+ }
2765
3100
  recomputeMicRequirements() {
2766
3101
  let anyPcm = false;
2767
3102
  let anyLc3 = false;
@@ -2773,7 +3108,11 @@ class LocalMiniappRuntime {
2773
3108
  if (stream.startsWith("transcription:") || stream.startsWith("translation:") || stream === "vad")
2774
3109
  anyLc3 = true;
2775
3110
  }
2776
- micStateCoordinator.setLocalRequirements({ pcm: anyPcm, lc3: anyLc3 });
3111
+ micStateCoordinator.setLocalRequirements({
3112
+ pcm: anyPcm,
3113
+ lc3: anyLc3,
3114
+ ...this.getConfiguredMicGates(),
3115
+ });
2777
3116
  }
2778
3117
  /**
2779
3118
  * Recompute the aggregated v2 cloud subscription list across all local miniapps.
@@ -2844,11 +3183,20 @@ class LocalMiniappRuntime {
2844
3183
  */
2845
3184
  buildCloudAudioSubscriptions(cloudStreams) {
2846
3185
  const subs = [];
3186
+ // Union of every connected app's TRANSCRIPTION_CONFIG hints. Hints only
3187
+ // apply to the auto-mode TRANSCRIPTION subscription (the wire schema has
3188
+ // no hints field on specific-language sources — the language IS the hint
3189
+ // there), and are deliberately NOT attached to translation sources.
3190
+ const hintUnion = [...new Set([...this.transcriptionHintsByApp.values()].flat())].sort();
2847
3191
  const langSource = (code) => code === "auto" || code === "*" ? { mode: "auto" } : { mode: "specific", code };
2848
3192
  for (const stream of cloudStreams) {
2849
3193
  if (stream.startsWith("transcription:")) {
2850
3194
  const lang = stream.substring("transcription:".length);
2851
- subs.push({ kind: "transcription", language: langSource(lang) });
3195
+ const source = langSource(lang);
3196
+ subs.push({
3197
+ kind: "transcription",
3198
+ language: source.mode === "auto" && hintUnion.length > 0 ? { mode: "auto", hints: hintUnion } : source,
3199
+ });
2852
3200
  }
2853
3201
  else if (stream.startsWith("translation:")) {
2854
3202
  const parts = stream.split(":");
@@ -3623,15 +3971,15 @@ class LocalMiniappRuntime {
3623
3971
  }
3624
3972
  }
3625
3973
  doPingRound() {
3626
- const now = Date.now();
3627
- const staleThreshold = PING_INTERVAL_MS * PING_TIMEOUT_THRESHOLD;
3628
3974
  const toRemove = [];
3629
3975
  for (const [packageName, app] of this.connectedApps) {
3630
- if (now - app.lastPongAt > staleThreshold) {
3976
+ const liveness = advanceMiniappPingLiveness(app.unansweredPingRounds, PING_TIMEOUT_THRESHOLD);
3977
+ if (liveness.shouldUnregister) {
3631
3978
  console.warn(`${LOG_TAG}: ${packageName} missed ${PING_TIMEOUT_THRESHOLD} pings, unregistering`);
3632
3979
  toRemove.push(packageName);
3633
3980
  continue;
3634
3981
  }
3982
+ app.unansweredPingRounds = liveness.unansweredPingRounds;
3635
3983
  // Send PING — SDK auto-replies with PONG
3636
3984
  this.sendToMiniapp(packageName, {
3637
3985
  type: MiniappRequestType.PING,
@@ -3652,6 +4000,7 @@ class LocalMiniappRuntime {
3652
4000
  const app = this.connectedApps.get(packageName);
3653
4001
  if (app) {
3654
4002
  app.lastPongAt = Date.now();
4003
+ app.unansweredPingRounds = 0;
3655
4004
  this.clearForegroundProbe(packageName);
3656
4005
  }
3657
4006
  }
@@ -3675,7 +4024,18 @@ class LocalMiniappRuntime {
3675
4024
  }
3676
4025
  // Belt-and-suspenders: clear any remaining state
3677
4026
  this.pendingCloudRequests.clear();
4027
+ const hadButtonPressSubscribers = this.getButtonPressSubscribers().length > 0;
3678
4028
  this.streamSubscribers.clear();
4029
+ if (hadButtonPressSubscribers) {
4030
+ for (const listener of this.buttonPressSubscriberListeners) {
4031
+ try {
4032
+ listener([]);
4033
+ }
4034
+ catch (error) {
4035
+ console.warn(`${LOG_TAG}: button press subscriber listener threw during cleanup:`, error);
4036
+ }
4037
+ }
4038
+ }
3679
4039
  this.connectedApps.clear();
3680
4040
  for (const timerId of this.foregroundProbeTimers.values()) {
3681
4041
  BgTimer.clearTimeout(timerId);