@mentra/engine 0.1.0-dev.0 → 3.1.0-dev.4

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 (503) hide show
  1. package/README.md +55 -18
  2. package/build/bluetooth-sdk/debug.d.ts +2 -0
  3. package/build/bluetooth-sdk/debug.d.ts.map +1 -0
  4. package/build/bluetooth-sdk/debug.js +2 -0
  5. package/build/bluetooth-sdk/debug.js.map +1 -0
  6. package/build/bluetooth-sdk/index.d.ts +3 -0
  7. package/build/bluetooth-sdk/index.d.ts.map +1 -0
  8. package/build/bluetooth-sdk/index.js +3 -0
  9. package/build/bluetooth-sdk/index.js.map +1 -0
  10. package/build/bluetooth-sdk/photo-receiver.d.ts +3 -0
  11. package/build/bluetooth-sdk/photo-receiver.d.ts.map +1 -0
  12. package/build/bluetooth-sdk/photo-receiver.js +3 -0
  13. package/build/bluetooth-sdk/photo-receiver.js.map +1 -0
  14. package/build/bluetooth-sdk/react.d.ts +2 -0
  15. package/build/bluetooth-sdk/react.d.ts.map +1 -0
  16. package/build/bluetooth-sdk/react.js +2 -0
  17. package/build/bluetooth-sdk/react.js.map +1 -0
  18. package/build/bluetooth-sdk/types.d.ts +2 -0
  19. package/build/bluetooth-sdk/types.d.ts.map +1 -0
  20. package/build/bluetooth-sdk/types.js +2 -0
  21. package/build/bluetooth-sdk/types.js.map +1 -0
  22. package/build/engine.d.ts +24 -16
  23. package/build/engine.d.ts.map +1 -1
  24. package/build/engine.js +9 -4
  25. package/build/engine.js.map +1 -1
  26. package/build/facades/dev.d.ts +3 -2
  27. package/build/facades/dev.d.ts.map +1 -1
  28. package/build/facades/dev.js +18 -12
  29. package/build/facades/dev.js.map +1 -1
  30. package/build/facades/display.d.ts +1 -1
  31. package/build/facades/display.d.ts.map +1 -1
  32. package/build/facades/display.js +1 -1
  33. package/build/facades/display.js.map +1 -1
  34. package/build/facades/gallery.js +1 -1
  35. package/build/facades/gallery.js.map +1 -1
  36. package/build/facades/glasses.d.ts +4 -13
  37. package/build/facades/glasses.d.ts.map +1 -1
  38. package/build/facades/glasses.js +21 -6
  39. package/build/facades/glasses.js.map +1 -1
  40. package/build/facades/glassesSettings.d.ts.map +1 -1
  41. package/build/facades/glassesSettings.js +1 -0
  42. package/build/facades/glassesSettings.js.map +1 -1
  43. package/build/facades/miniapps.d.ts +4 -0
  44. package/build/facades/miniapps.d.ts.map +1 -1
  45. package/build/facades/miniapps.js +5 -0
  46. package/build/facades/miniapps.js.map +1 -1
  47. package/build/facades/ota.d.ts +14 -16
  48. package/build/facades/ota.d.ts.map +1 -1
  49. package/build/facades/ota.js +22 -7
  50. package/build/facades/ota.js.map +1 -1
  51. package/build/facades/pairing.d.ts +8 -5
  52. package/build/facades/pairing.d.ts.map +1 -1
  53. package/build/facades/pairing.js +12 -10
  54. package/build/facades/pairing.js.map +1 -1
  55. package/build/facades/permissions.d.ts +10 -0
  56. package/build/facades/permissions.d.ts.map +1 -1
  57. package/build/facades/permissions.js +10 -0
  58. package/build/facades/permissions.js.map +1 -1
  59. package/build/facades/phoneNotifications.d.ts +0 -4
  60. package/build/facades/phoneNotifications.d.ts.map +1 -1
  61. package/build/facades/phoneNotifications.js +4 -14
  62. package/build/facades/phoneNotifications.js.map +1 -1
  63. package/build/facades/speech.d.ts +7 -6
  64. package/build/facades/speech.d.ts.map +1 -1
  65. package/build/facades/speech.js +1 -1
  66. package/build/facades/speech.js.map +1 -1
  67. package/build/generated/releaseMetadata.d.ts +12 -0
  68. package/build/generated/releaseMetadata.d.ts.map +1 -0
  69. package/build/generated/releaseMetadata.js +10 -0
  70. package/build/generated/releaseMetadata.js.map +1 -0
  71. package/build/index.d.ts +10 -8
  72. package/build/index.d.ts.map +1 -1
  73. package/build/index.js +6 -5
  74. package/build/index.js.map +1 -1
  75. package/build/react/MentraLiveOtaFlow.d.ts +38 -0
  76. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  77. package/build/react/MentraLiveOtaFlow.js +528 -0
  78. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  79. package/build/react/index.d.ts +2 -0
  80. package/build/react/index.d.ts.map +1 -0
  81. package/build/react/index.js +2 -0
  82. package/build/react/index.js.map +1 -0
  83. package/build/react/useEngineSnapshot.d.ts +2 -0
  84. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  85. package/build/react/useEngineSnapshot.js +14 -0
  86. package/build/react/useEngineSnapshot.js.map +1 -0
  87. package/build/runtime/bootstrap.d.ts +24 -1
  88. package/build/runtime/bootstrap.d.ts.map +1 -1
  89. package/build/runtime/bootstrap.js +17 -0
  90. package/build/runtime/bootstrap.js.map +1 -1
  91. package/build/runtime/scanQrSeam.d.ts +7 -0
  92. package/build/runtime/scanQrSeam.d.ts.map +1 -0
  93. package/build/runtime/scanQrSeam.js +16 -0
  94. package/build/runtime/scanQrSeam.js.map +1 -0
  95. package/build/services/AppRegistry.d.ts +41 -57
  96. package/build/services/AppRegistry.d.ts.map +1 -1
  97. package/build/services/AppRegistry.js +208 -144
  98. package/build/services/AppRegistry.js.map +1 -1
  99. package/build/services/AudioCloudUplink.d.ts +12 -0
  100. package/build/services/AudioCloudUplink.d.ts.map +1 -1
  101. package/build/services/AudioCloudUplink.js +22 -1
  102. package/build/services/AudioCloudUplink.js.map +1 -1
  103. package/build/services/AudioPlaybackService.d.ts +41 -2
  104. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  105. package/build/services/AudioPlaybackService.js +250 -33
  106. package/build/services/AudioPlaybackService.js.map +1 -1
  107. package/build/services/BlobStore.d.ts +2 -1
  108. package/build/services/BlobStore.d.ts.map +1 -1
  109. package/build/services/BlobStore.js +17 -9
  110. package/build/services/BlobStore.js.map +1 -1
  111. package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
  112. package/build/services/CloudAudioSubscriptionSync.js +13 -1
  113. package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
  114. package/build/services/CloudClientService.d.ts +15 -0
  115. package/build/services/CloudClientService.d.ts.map +1 -1
  116. package/build/services/CloudClientService.js +93 -1
  117. package/build/services/CloudClientService.js.map +1 -1
  118. package/build/services/DeviceEventRouter.js +5 -5
  119. package/build/services/DeviceEventRouter.js.map +1 -1
  120. package/build/services/ForegroundLocationPermission.d.ts +13 -0
  121. package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
  122. package/build/services/ForegroundLocationPermission.js +12 -0
  123. package/build/services/ForegroundLocationPermission.js.map +1 -0
  124. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  125. package/build/services/GlassesSettingsSync.js +9 -3
  126. package/build/services/GlassesSettingsSync.js.map +1 -1
  127. package/build/services/GlassesStatusProjection.d.ts +2 -1
  128. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  129. package/build/services/GlassesStatusProjection.js +15 -8
  130. package/build/services/GlassesStatusProjection.js.map +1 -1
  131. package/build/services/HotspotOtaTransport.d.ts +29 -0
  132. package/build/services/HotspotOtaTransport.d.ts.map +1 -0
  133. package/build/services/HotspotOtaTransport.js +146 -0
  134. package/build/services/HotspotOtaTransport.js.map +1 -0
  135. package/build/services/HotspotShutdown.d.ts +17 -0
  136. package/build/services/HotspotShutdown.d.ts.map +1 -0
  137. package/build/services/HotspotShutdown.js +29 -0
  138. package/build/services/HotspotShutdown.js.map +1 -0
  139. package/build/services/LocalDisplayManager.d.ts +15 -0
  140. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  141. package/build/services/LocalDisplayManager.js +106 -20
  142. package/build/services/LocalDisplayManager.js.map +1 -1
  143. package/build/services/LocalMiniappRuntime.d.ts +74 -2
  144. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  145. package/build/services/LocalMiniappRuntime.js +785 -291
  146. package/build/services/LocalMiniappRuntime.js.map +1 -1
  147. package/build/services/LocalMiniappStorage.d.ts +32 -0
  148. package/build/services/LocalMiniappStorage.d.ts.map +1 -0
  149. package/build/services/LocalMiniappStorage.js +66 -0
  150. package/build/services/LocalMiniappStorage.js.map +1 -0
  151. package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
  152. package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
  153. package/build/services/LocalMiniappUserIdentity.js +43 -0
  154. package/build/services/LocalMiniappUserIdentity.js.map +1 -0
  155. package/build/services/LocalSttFallbackCoordinator.d.ts +9 -6
  156. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  157. package/build/services/LocalSttFallbackCoordinator.js +18 -12
  158. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  159. package/build/services/MicStateCoordinator.d.ts +42 -1
  160. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  161. package/build/services/MicStateCoordinator.js +141 -3
  162. package/build/services/MicStateCoordinator.js.map +1 -1
  163. package/build/services/MiniappLauncher.d.ts.map +1 -1
  164. package/build/services/MiniappLauncher.js +23 -3
  165. package/build/services/MiniappLauncher.js.map +1 -1
  166. package/build/services/MiniappLiveness.d.ts +12 -0
  167. package/build/services/MiniappLiveness.d.ts.map +1 -0
  168. package/build/services/MiniappLiveness.js +16 -0
  169. package/build/services/MiniappLiveness.js.map +1 -0
  170. package/build/services/NativeHttpResponse.d.ts +3 -0
  171. package/build/services/NativeHttpResponse.d.ts.map +1 -0
  172. package/build/services/NativeHttpResponse.js +6 -0
  173. package/build/services/NativeHttpResponse.js.map +1 -0
  174. package/build/services/OtaArtifactDownloader.d.ts +65 -0
  175. package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
  176. package/build/services/OtaArtifactDownloader.js +226 -0
  177. package/build/services/OtaArtifactDownloader.js.map +1 -0
  178. package/build/services/OtaAutoChain.d.ts +34 -0
  179. package/build/services/OtaAutoChain.d.ts.map +1 -0
  180. package/build/services/OtaAutoChain.js +83 -0
  181. package/build/services/OtaAutoChain.js.map +1 -0
  182. package/build/services/OtaErrorMapping.d.ts +18 -0
  183. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  184. package/build/services/OtaErrorMapping.js +63 -0
  185. package/build/services/OtaErrorMapping.js.map +1 -0
  186. package/build/services/OtaInstallCoordinator.d.ts +95 -15
  187. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  188. package/build/services/OtaInstallCoordinator.js +577 -130
  189. package/build/services/OtaInstallCoordinator.js.map +1 -1
  190. package/build/services/OtaService.d.ts.map +1 -1
  191. package/build/services/OtaService.js +8 -1
  192. package/build/services/OtaService.js.map +1 -1
  193. package/build/services/OtaUpdateCheckService.d.ts +39 -4
  194. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  195. package/build/services/OtaUpdateCheckService.js +138 -22
  196. package/build/services/OtaUpdateCheckService.js.map +1 -1
  197. package/build/services/PhoneCalendarHelpers.d.ts +32 -0
  198. package/build/services/PhoneCalendarHelpers.d.ts.map +1 -0
  199. package/build/services/PhoneCalendarHelpers.js +157 -0
  200. package/build/services/PhoneCalendarHelpers.js.map +1 -0
  201. package/build/services/PhoneCalendarService.d.ts +5 -0
  202. package/build/services/PhoneCalendarService.d.ts.map +1 -0
  203. package/build/services/PhoneCalendarService.js +45 -0
  204. package/build/services/PhoneCalendarService.js.map +1 -0
  205. package/build/services/PhoneCameraFovCoordinator.d.ts +30 -0
  206. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -0
  207. package/build/services/PhoneCameraFovCoordinator.js +194 -0
  208. package/build/services/PhoneCameraFovCoordinator.js.map +1 -0
  209. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  210. package/build/services/PhoneNotificationsSync.js +12 -10
  211. package/build/services/PhoneNotificationsSync.js.map +1 -1
  212. package/build/services/PhonePhotoCoordinator.d.ts +26 -2
  213. package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
  214. package/build/services/PhonePhotoCoordinator.js +79 -9
  215. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  216. package/build/services/PhoneStreamCoordinator.d.ts +8 -1
  217. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  218. package/build/services/PhoneStreamCoordinator.js +125 -13
  219. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  220. package/build/services/PhoneVideoCoordinator.d.ts +2 -0
  221. package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
  222. package/build/services/PhoneVideoCoordinator.js +10 -0
  223. package/build/services/PhoneVideoCoordinator.js.map +1 -1
  224. package/build/services/SceneRenderer.d.ts.map +1 -1
  225. package/build/services/SceneRenderer.js +15 -0
  226. package/build/services/SceneRenderer.js.map +1 -1
  227. package/build/services/SentenceTtsPipeline.d.ts +31 -0
  228. package/build/services/SentenceTtsPipeline.d.ts.map +1 -0
  229. package/build/services/SentenceTtsPipeline.js +67 -0
  230. package/build/services/SentenceTtsPipeline.js.map +1 -0
  231. package/build/services/SupportProfileSync.d.ts +22 -0
  232. package/build/services/SupportProfileSync.d.ts.map +1 -0
  233. package/build/services/SupportProfileSync.js +213 -0
  234. package/build/services/SupportProfileSync.js.map +1 -0
  235. package/build/services/TranscriptionRouting.d.ts +12 -0
  236. package/build/services/TranscriptionRouting.d.ts.map +1 -0
  237. package/build/services/TranscriptionRouting.js +24 -0
  238. package/build/services/TranscriptionRouting.js.map +1 -0
  239. package/build/services/TtsTextSanitizer.d.ts +5 -0
  240. package/build/services/TtsTextSanitizer.d.ts.map +1 -0
  241. package/build/services/TtsTextSanitizer.js +61 -0
  242. package/build/services/TtsTextSanitizer.js.map +1 -0
  243. package/build/services/WifiAdbRequest.d.ts +17 -0
  244. package/build/services/WifiAdbRequest.d.ts.map +1 -0
  245. package/build/services/WifiAdbRequest.js +35 -0
  246. package/build/services/WifiAdbRequest.js.map +1 -0
  247. package/build/services/asg/asgCameraApi.d.ts +43 -0
  248. package/build/services/asg/asgCameraApi.d.ts.map +1 -1
  249. package/build/services/asg/asgCameraApi.js +509 -26
  250. package/build/services/asg/asgCameraApi.js.map +1 -1
  251. package/build/services/asg/cameraRollExportCoordinator.d.ts +60 -0
  252. package/build/services/asg/cameraRollExportCoordinator.d.ts.map +1 -0
  253. package/build/services/asg/cameraRollExportCoordinator.js +618 -0
  254. package/build/services/asg/cameraRollExportCoordinator.js.map +1 -0
  255. package/build/services/asg/cameraRollExportLedger.d.ts +44 -0
  256. package/build/services/asg/cameraRollExportLedger.d.ts.map +1 -0
  257. package/build/services/asg/cameraRollExportLedger.js +78 -0
  258. package/build/services/asg/cameraRollExportLedger.js.map +1 -0
  259. package/build/services/asg/galleryNotices.d.ts +1 -1
  260. package/build/services/asg/galleryNotices.d.ts.map +1 -1
  261. package/build/services/asg/galleryNotices.js.map +1 -1
  262. package/build/services/asg/galleryPathMigration.d.ts +3 -0
  263. package/build/services/asg/galleryPathMigration.d.ts.map +1 -0
  264. package/build/services/asg/galleryPathMigration.js +26 -0
  265. package/build/services/asg/galleryPathMigration.js.map +1 -0
  266. package/build/services/asg/gallerySettingsService.d.ts +1 -0
  267. package/build/services/asg/gallerySettingsService.d.ts.map +1 -1
  268. package/build/services/asg/gallerySettingsService.js +2 -0
  269. package/build/services/asg/gallerySettingsService.js.map +1 -1
  270. package/build/services/asg/gallerySyncService.d.ts.map +1 -1
  271. package/build/services/asg/gallerySyncService.js +162 -56
  272. package/build/services/asg/gallerySyncService.js.map +1 -1
  273. package/build/services/asg/galleryTransferLedger.d.ts +58 -0
  274. package/build/services/asg/galleryTransferLedger.d.ts.map +1 -0
  275. package/build/services/asg/galleryTransferLedger.js +225 -0
  276. package/build/services/asg/galleryTransferLedger.js.map +1 -0
  277. package/build/services/asg/localNetworkTransport.d.ts +15 -0
  278. package/build/services/asg/localNetworkTransport.d.ts.map +1 -0
  279. package/build/services/asg/localNetworkTransport.js +144 -0
  280. package/build/services/asg/localNetworkTransport.js.map +1 -0
  281. package/build/services/asg/localStorageService.d.ts +18 -0
  282. package/build/services/asg/localStorageService.d.ts.map +1 -1
  283. package/build/services/asg/localStorageService.js +168 -19
  284. package/build/services/asg/localStorageService.js.map +1 -1
  285. package/build/services/asg/mediaProcessingQueue.d.ts +10 -0
  286. package/build/services/asg/mediaProcessingQueue.d.ts.map +1 -1
  287. package/build/services/asg/mediaProcessingQueue.js +193 -62
  288. package/build/services/asg/mediaProcessingQueue.js.map +1 -1
  289. package/build/services/audioPlaybackAssets.d.ts +8 -0
  290. package/build/services/audioPlaybackAssets.d.ts.map +1 -0
  291. package/build/services/audioPlaybackAssets.js +7 -0
  292. package/build/services/audioPlaybackAssets.js.map +1 -0
  293. package/build/services/gallerySyncClock.js +1 -1
  294. package/build/services/gallerySyncClock.js.map +1 -1
  295. package/build/services/glassesClockSync.d.ts.map +1 -1
  296. package/build/services/glassesClockSync.js +6 -2
  297. package/build/services/glassesClockSync.js.map +1 -1
  298. package/build/services/manifestActions.d.ts +7 -0
  299. package/build/services/manifestActions.d.ts.map +1 -0
  300. package/build/services/manifestActions.js +25 -0
  301. package/build/services/manifestActions.js.map +1 -0
  302. package/build/services/otaDisplayState.d.ts +23 -12
  303. package/build/services/otaDisplayState.d.ts.map +1 -1
  304. package/build/services/otaDisplayState.js +36 -14
  305. package/build/services/otaDisplayState.js.map +1 -1
  306. package/build/services/otaInstallPolicy.d.ts +16 -4
  307. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  308. package/build/services/otaInstallPolicy.js +24 -3
  309. package/build/services/otaInstallPolicy.js.map +1 -1
  310. package/build/services/otaLegacyMapping.d.ts +1 -1
  311. package/build/services/otaLegacyMapping.d.ts.map +1 -1
  312. package/build/services/otaLegacyMapping.js.map +1 -1
  313. package/build/services/otaManifestPolicy.d.ts +14 -0
  314. package/build/services/otaManifestPolicy.d.ts.map +1 -0
  315. package/build/services/otaManifestPolicy.js +19 -0
  316. package/build/services/otaManifestPolicy.js.map +1 -0
  317. package/build/services/otaManifestUrl.d.ts +3 -0
  318. package/build/services/otaManifestUrl.d.ts.map +1 -0
  319. package/build/services/otaManifestUrl.js +38 -0
  320. package/build/services/otaManifestUrl.js.map +1 -0
  321. package/build/services/slimStreamStatus.d.ts +11 -0
  322. package/build/services/slimStreamStatus.d.ts.map +1 -1
  323. package/build/services/slimStreamStatus.js +14 -4
  324. package/build/services/slimStreamStatus.js.map +1 -1
  325. package/build/stores/apps.d.ts.map +1 -1
  326. package/build/stores/apps.js +11 -0
  327. package/build/stores/apps.js.map +1 -1
  328. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  329. package/build/stores/bluetoothSettingKeys.js +2 -0
  330. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  331. package/build/stores/gallerySync.d.ts +2 -0
  332. package/build/stores/gallerySync.d.ts.map +1 -1
  333. package/build/stores/gallerySync.js +2 -0
  334. package/build/stores/gallerySync.js.map +1 -1
  335. package/build/stores/glasses.d.ts.map +1 -1
  336. package/build/stores/glasses.js +3 -0
  337. package/build/stores/glasses.js.map +1 -1
  338. package/build/stores/settings.d.ts +17 -0
  339. package/build/stores/settings.d.ts.map +1 -1
  340. package/build/stores/settings.js +157 -17
  341. package/build/stores/settings.js.map +1 -1
  342. package/build/types/applet.d.ts +2 -0
  343. package/build/types/applet.d.ts.map +1 -1
  344. package/build/types/applet.js.map +1 -1
  345. package/build/types/asg.d.ts +7 -0
  346. package/build/types/asg.d.ts.map +1 -1
  347. package/build/types/asg.js.map +1 -1
  348. package/build/types/enums.d.ts +2 -25
  349. package/build/types/enums.d.ts.map +1 -1
  350. package/build/types/enums.js +2 -27
  351. package/build/types/enums.js.map +1 -1
  352. package/build/types/hardware.d.ts +4 -13
  353. package/build/types/hardware.d.ts.map +1 -1
  354. package/build/types/hardware.js +15 -3
  355. package/build/types/hardware.js.map +1 -1
  356. package/build/types/index.d.ts +2 -6
  357. package/build/types/index.d.ts.map +1 -1
  358. package/build/types/index.js +2 -6
  359. package/build/types/index.js.map +1 -1
  360. package/build/utils/configuredDevHost.d.ts +6 -0
  361. package/build/utils/configuredDevHost.d.ts.map +1 -0
  362. package/build/utils/configuredDevHost.js +19 -0
  363. package/build/utils/configuredDevHost.js.map +1 -0
  364. package/build/utils/devMiniappLaunch.d.ts +19 -8
  365. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  366. package/build/utils/devMiniappLaunch.js +144 -21
  367. package/build/utils/devMiniappLaunch.js.map +1 -1
  368. package/build/utils/diagnosticContext.d.ts.map +1 -1
  369. package/build/utils/diagnosticContext.js +15 -17
  370. package/build/utils/diagnosticContext.js.map +1 -1
  371. package/build/utils/hardware/hardware.d.ts.map +1 -1
  372. package/build/utils/hardware/hardware.js +7 -3
  373. package/build/utils/hardware/hardware.js.map +1 -1
  374. package/build/utils/miniappDiagnostics.d.ts +64 -0
  375. package/build/utils/miniappDiagnostics.d.ts.map +1 -0
  376. package/build/utils/miniappDiagnostics.js +173 -0
  377. package/build/utils/miniappDiagnostics.js.map +1 -0
  378. package/build/utils/permissions/MediaLibraryPermissions.d.ts +11 -0
  379. package/build/utils/permissions/MediaLibraryPermissions.d.ts.map +1 -1
  380. package/build/utils/permissions/MediaLibraryPermissions.js +27 -4
  381. package/build/utils/permissions/MediaLibraryPermissions.js.map +1 -1
  382. package/build/utils/timers/timers.d.ts.map +1 -1
  383. package/build/utils/timers/timers.js +55 -18
  384. package/build/utils/timers/timers.js.map +1 -1
  385. package/package.json +59 -31
  386. package/src/assets/silence.wav +0 -0
  387. package/src/bluetooth-sdk/debug.ts +1 -0
  388. package/src/bluetooth-sdk/index.ts +2 -0
  389. package/src/bluetooth-sdk/photo-receiver.ts +2 -0
  390. package/src/bluetooth-sdk/react.ts +1 -0
  391. package/src/bluetooth-sdk/types.ts +1 -0
  392. package/src/engine.ts +9 -4
  393. package/src/facades/dev.ts +17 -12
  394. package/src/facades/display.ts +1 -1
  395. package/src/facades/gallery.ts +1 -1
  396. package/src/facades/glasses.ts +20 -7
  397. package/src/facades/glassesSettings.ts +5 -0
  398. package/src/facades/miniapps.ts +7 -0
  399. package/src/facades/ota.ts +23 -8
  400. package/src/facades/pairing.ts +15 -11
  401. package/src/facades/permissions.ts +14 -0
  402. package/src/facades/phoneNotifications.ts +4 -13
  403. package/src/facades/speech.ts +21 -7
  404. package/src/generated/releaseMetadata.ts +20 -0
  405. package/src/index.ts +39 -13
  406. package/src/react/MentraLiveOtaFlow.tsx +856 -0
  407. package/src/react/index.ts +7 -0
  408. package/src/react/useEngineSnapshot.ts +16 -0
  409. package/src/runtime/bootstrap.ts +33 -1
  410. package/src/runtime/scanQrSeam.ts +20 -0
  411. package/src/services/AppRegistry.ts +240 -149
  412. package/src/services/AudioCloudUplink.ts +23 -1
  413. package/src/services/AudioPlaybackService.ts +287 -38
  414. package/src/services/BlobStore.ts +22 -9
  415. package/src/services/CloudAudioSubscriptionSync.ts +13 -1
  416. package/src/services/CloudClientService.ts +96 -1
  417. package/src/services/DeviceEventRouter.ts +5 -5
  418. package/src/services/ForegroundLocationPermission.ts +24 -0
  419. package/src/services/GlassesSettingsSync.ts +9 -3
  420. package/src/services/GlassesStatusProjection.ts +16 -8
  421. package/src/services/HotspotOtaTransport.ts +184 -0
  422. package/src/services/HotspotShutdown.ts +41 -0
  423. package/src/services/LocalDisplayManager.ts +112 -21
  424. package/src/services/LocalMiniappRuntime.ts +967 -346
  425. package/src/services/LocalMiniappStorage.ts +82 -0
  426. package/src/services/LocalMiniappUserIdentity.ts +46 -0
  427. package/src/services/LocalSttFallbackCoordinator.ts +19 -12
  428. package/src/services/MicStateCoordinator.ts +169 -4
  429. package/src/services/MiniappLauncher.ts +30 -3
  430. package/src/services/MiniappLiveness.ts +24 -0
  431. package/src/services/NativeHttpResponse.ts +6 -0
  432. package/src/services/OtaArtifactDownloader.ts +312 -0
  433. package/src/services/OtaAutoChain.ts +106 -0
  434. package/src/services/OtaErrorMapping.ts +80 -0
  435. package/src/services/OtaInstallCoordinator.ts +649 -144
  436. package/src/services/OtaService.ts +11 -5
  437. package/src/services/OtaUpdateCheckService.ts +181 -21
  438. package/src/services/PhoneCalendarHelpers.ts +212 -0
  439. package/src/services/PhoneCalendarService.ts +65 -0
  440. package/src/services/PhoneCameraFovCoordinator.ts +210 -0
  441. package/src/services/PhoneNotificationsSync.ts +12 -10
  442. package/src/services/PhonePhotoCoordinator.ts +116 -18
  443. package/src/services/PhoneStreamCoordinator.ts +139 -25
  444. package/src/services/PhoneVideoCoordinator.ts +11 -0
  445. package/src/services/SceneRenderer.ts +16 -0
  446. package/src/services/SentenceTtsPipeline.ts +106 -0
  447. package/src/services/SupportProfileSync.ts +218 -0
  448. package/src/services/TranscriptionRouting.ts +37 -0
  449. package/src/services/TtsTextSanitizer.ts +66 -0
  450. package/src/services/WifiAdbRequest.ts +53 -0
  451. package/src/services/asg/asgCameraApi.ts +615 -33
  452. package/src/services/asg/cameraRollExportCoordinator.ts +627 -0
  453. package/src/services/asg/cameraRollExportLedger.ts +122 -0
  454. package/src/services/asg/galleryNotices.ts +1 -0
  455. package/src/services/asg/galleryPathMigration.ts +22 -0
  456. package/src/services/asg/gallerySettingsService.ts +3 -0
  457. package/src/services/asg/gallerySyncService.ts +184 -73
  458. package/src/services/asg/galleryTransferLedger.ts +312 -0
  459. package/src/services/asg/localNetworkTransport.ts +169 -0
  460. package/src/services/asg/localStorageService.ts +179 -24
  461. package/src/services/asg/mediaProcessingQueue.ts +209 -62
  462. package/src/services/audioPlaybackAssets.ts +8 -0
  463. package/src/services/gallerySyncClock.ts +1 -1
  464. package/src/services/glassesClockSync.ts +6 -2
  465. package/src/services/manifestActions.ts +30 -0
  466. package/src/services/otaDisplayState.ts +58 -17
  467. package/src/services/otaInstallPolicy.ts +30 -4
  468. package/src/services/otaLegacyMapping.ts +1 -1
  469. package/src/services/otaManifestPolicy.ts +42 -0
  470. package/src/services/otaManifestUrl.ts +44 -0
  471. package/src/services/slimStreamStatus.ts +16 -5
  472. package/src/stores/apps.ts +12 -0
  473. package/src/stores/bluetoothSettingKeys.ts +2 -0
  474. package/src/stores/gallerySync.ts +4 -0
  475. package/src/stores/glasses.ts +3 -0
  476. package/src/stores/settings.ts +179 -18
  477. package/src/types/applet.ts +2 -0
  478. package/src/types/asg.ts +7 -0
  479. package/src/types/enums.ts +3 -28
  480. package/src/types/hardware.ts +20 -13
  481. package/src/types/index.ts +2 -6
  482. package/src/utils/configuredDevHost.ts +17 -0
  483. package/src/utils/devMiniappLaunch.ts +151 -23
  484. package/src/utils/diagnosticContext.ts +17 -17
  485. package/src/utils/hardware/hardware.ts +7 -9
  486. package/src/utils/miniappDiagnostics.ts +226 -0
  487. package/src/utils/permissions/MediaLibraryPermissions.ts +35 -4
  488. package/src/utils/timers/timers.ts +57 -21
  489. package/build/devtools.d.ts +0 -11
  490. package/build/devtools.d.ts.map +0 -1
  491. package/build/devtools.js +0 -13
  492. package/build/devtools.js.map +0 -1
  493. package/build/internal.d.ts +0 -66
  494. package/build/internal.d.ts.map +0 -1
  495. package/build/internal.js +0 -89
  496. package/build/internal.js.map +0 -1
  497. package/build/services/asgOtaVersionUrl.d.ts +0 -2
  498. package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
  499. package/build/services/asgOtaVersionUrl.js +0 -36
  500. package/build/services/asgOtaVersionUrl.js.map +0 -1
  501. package/src/devtools.ts +0 -13
  502. package/src/internal.ts +0 -158
  503. package/src/services/asgOtaVersionUrl.ts +0 -41
@@ -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";
@@ -32,18 +32,30 @@ import localSttFallbackCoordinator from "./LocalSttFallbackCoordinator";
32
32
  import micStateCoordinator from "./MicStateCoordinator";
33
33
  import { BlobStore } from "./BlobStore";
34
34
  import { CloudAudioSubscriptionSync } from "./CloudAudioSubscriptionSync";
35
+ import { phoneCameraFovCoordinator } from "./PhoneCameraFovCoordinator";
35
36
  import { phonePhotoCoordinator } from "./PhonePhotoCoordinator";
36
37
  import { phoneStreamCoordinator } from "./PhoneStreamCoordinator";
37
38
  import { phoneVideoCoordinator } from "./PhoneVideoCoordinator";
39
+ import { runSentenceTtsPipeline } from "./SentenceTtsPipeline";
40
+ import { summarizeTranscriptionRoutes, transcriptionDeliveryRoute } from "./TranscriptionRouting";
41
+ import { prepareTtsSentences } from "./TtsTextSanitizer";
42
+ import { handleWifiAdbRequest } from "./WifiAdbRequest";
38
43
  import { cloudClientService } from "./CloudClientService";
39
44
  import { miniappLauncher } from "./MiniappLauncher";
40
45
  import { ISLAND_SETTINGS_KEYS, } from "../runtime/config";
41
46
  import { getAnalytics, getUiSeams } from "../runtime/bootstrap";
47
+ import { invokeScanQrSeam } from "../runtime/scanQrSeam";
42
48
  import { normalizeStreamAudioConfig, normalizeStreamVideoConfig } from "../runtime/streamConfig";
49
+ import { toLanguageHint } from "@mentra/cloud-protocol/languages";
50
+ import { buildMiniappManifestSnapshot } from "../utils/miniappDiagnostics";
43
51
  import ttsModelManager from "./TTSModelManager";
44
52
  import { NavigationHandlers } from "./NavigationHandlers";
45
53
  import { useAppStatusStore } from "../stores/apps";
46
- import { DEV_APP_PACKAGE_NAME, getDevAppSourcePackage } from "./AppRegistry";
54
+ import { getDevAppAttestation, getDevAppSourcePackage } from "./AppRegistry";
55
+ import { resolveForegroundLocationPermission } from "./ForegroundLocationPermission";
56
+ import { advanceMiniappPingLiveness } from "./MiniappLiveness";
57
+ import { listPhoneCalendarEvents, PhoneCalendarError } from "./PhoneCalendarService";
58
+ import { LocalMiniappStorage } from "./LocalMiniappStorage";
47
59
  /**
48
60
  * Strictness ordering for `location_stream` rate values. Higher index =
49
61
  * higher accuracy and higher power cost. The aggregate tier applied to
@@ -76,6 +88,14 @@ function withTimeout(promise, timeoutMs) {
76
88
  });
77
89
  }
78
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
+ }
79
99
  const SYSTEM_MINIAPP_PACKAGES = new Set([
80
100
  "com.mentra.camera",
81
101
  "com.mentra.gallery",
@@ -106,76 +126,32 @@ const REQUEST_WIFI_SETUP_TYPE = "miniapp_request_wifi_setup";
106
126
  // respawn path wired (MentraJSRouter.start), a genuinely dead context still
107
127
  // comes back automatically — this threshold only bounds how long that takes.
108
128
  const PING_TIMEOUT_THRESHOLD = 6; // ~30s
109
- // ===========================================================================
110
- // BGCAP: temporary background-caption telemetry (DIAGNOSTIC — remove after the
111
- // "captions fall behind then flood in waves" investigation lands a fix).
112
- //
113
- // Captions now run as a local miniapp inside the per-context JS engine
114
- // (Crust: QuickJS on Android / JSC on iOS), driven off a single serial
115
- // queue/executor with NO background awareness. The hypothesis is that the OS
116
- // starves/freezes that queue when the screen is off (iOS app suspension /
117
- // Android Doze), so inbound transcript deliveries pile up and drain in a burst
118
- // on resume. These logs measure exactly that. All lines are prefixed "BGCAP:"
119
- // so they're trivial to grep in incident logs and to delete. Cross-platform —
120
- // host-side only, no native changes.
121
- // ===========================================================================
122
- const BGCAP_TELEMETRY = false;
123
129
  const TRANSCRIPT_TIMING_TELEMETRY = globalThis.__DEV__ === true;
124
- let bgcapHbTimer = null;
125
- let bgcapHbLast = 0;
126
- let bgcapTxCount = 0;
127
- let bgcapTxLastFinalAt = 0;
128
- let bgcapTxLastLogAt = 0;
129
- function bgcapStartHeartbeat() {
130
- if (!BGCAP_TELEMETRY || bgcapHbTimer !== null)
131
- return;
132
- bgcapHbLast = Date.now();
133
- // PLAIN setInterval (NOT BgTimer): a plain JS timer only fires when the RN
134
- // JS thread is actually executing. A gap >> 1s between ticks therefore proves
135
- // the JS thread was suspended/throttled (iOS suspend / Android Doze) for that
136
- // long — the smoking gun for the freeze. Silent when healthy; logs the stall
137
- // duration when it happens. Pair with the "App state changed to:" lines.
138
- bgcapHbTimer = setInterval(() => {
139
- const now = Date.now();
140
- const gap = now - bgcapHbLast;
141
- bgcapHbLast = now;
142
- if (gap > 2000) {
143
- console.log(`BGCAP: host-js stall — no tick for ${gap}ms (RN JS thread frozen/throttled)`);
144
- }
145
- }, 1000);
146
- }
147
- function bgcapStopHeartbeat() {
148
- if (bgcapHbTimer !== null) {
149
- clearInterval(bgcapHbTimer);
150
- bgcapHbTimer = null;
151
- }
152
- }
153
- // Rate-limited (1/s) summary of transcripts being FORWARDED to miniapps. Shows
154
- // whether transcripts keep arriving + being handed to the captions miniapp
155
- // during background. Compare its rate against the native "MAN: displayEvent"
156
- // (render) rate: fwd keeps flowing but render lags => miniapp executor is
157
- // starved; fwd itself stops => upstream (cloud delivery / RN thread) is the gap.
158
- function bgcapNoteTranscriptForward(isFinal) {
159
- if (!BGCAP_TELEMETRY)
160
- return;
161
- bgcapTxCount++;
162
- if (isFinal)
163
- bgcapTxLastFinalAt = Date.now();
164
- const now = Date.now();
165
- if (now - bgcapTxLastLogAt >= 1000) {
166
- console.log(`BGCAP: tx forwarded=${bgcapTxCount} in last ${now - bgcapTxLastLogAt}ms (lastFinal ${bgcapTxLastFinalAt ? now - bgcapTxLastFinalAt : -1}ms ago)`);
167
- bgcapTxCount = 0;
168
- bgcapTxLastLogAt = now;
169
- }
170
- }
171
130
  const RGB_LED_ACTIONS = new Set(["on", "off"]);
172
131
  const RGB_LED_COLORS = new Set(["red", "green", "blue", "orange", "white"]);
173
132
  const CAMERA_FOV_MIN = 62;
174
133
  const CAMERA_FOV_MAX = 118;
175
- const CAMERA_FOV_DEFAULT = 102;
176
- 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 };
177
136
  const CAMERA_ROI_POSITION_BY_NAME = { center: "center", bottom: "bottom", top: "top" };
178
- const CAMERA_ROI_POSITION_VALUES = { center: 0, bottom: 1, top: 2 };
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
+ };
179
155
  // =============================================================================
180
156
  // Declared-permission record helper (for CONNECT_ACK / PERMISSIONS_UPDATE)
181
157
  // =============================================================================
@@ -292,10 +268,19 @@ class LocalMiniappRuntime {
292
268
  connectWaiters = new Map();
293
269
  /** Ref-counted stream subscriptions: stream → set of packageNames. */
294
270
  streamSubscribers = new Map();
271
+ /** Host observers for the Mentra Live button-capture policy. */
272
+ buttonPressSubscriberListeners = new Set();
295
273
  /** Guards one-time wiring of the cloud transcript/translation fan-out. */
296
274
  cloudResultsWired = false;
297
275
  cloudStatusWired = false;
298
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();
299
284
  /** Ping interval handle. */
300
285
  pingIntervalId = null;
301
286
  foregroundProbeTimers = new Map();
@@ -310,12 +295,95 @@ class LocalMiniappRuntime {
310
295
  onLivenessTimeout = null;
311
296
  /** Pending cloud requests: requestId → packageName that originated the request. */
312
297
  pendingCloudRequests = new Map();
298
+ speechRuns = new Map();
313
299
  // Browser fallback token auth — HMAC-signed blob with a phone-local
314
300
  // secret. Both issuer and verifier are the same process, so the
315
301
  // secret never leaves the device.
316
302
  localSecret = `miniapp_${Date.now()}_${Math.random().toString(36).slice(2, 14)}`;
317
303
  usedTokens = new Set();
318
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
+ }
319
387
  /**
320
388
  * Generate an HMAC-signed local session token for browser fallback auth.
321
389
  * Token format: base64(JSON({userId, packageName, exp})).base64(HMAC-SHA256(payload, secret))
@@ -502,6 +570,7 @@ class LocalMiniappRuntime {
502
570
  // dangling references. The WebView will re-subscribe after its CONNECT.
503
571
  if (this.connectedApps.has(packageName)) {
504
572
  const existing = this.connectedApps.get(packageName);
573
+ this.cancelSpeech(packageName);
505
574
  // A crash respawn replaces the ConnectedMiniapp without going through
506
575
  // unregisterApp(). Abort the old incarnation's native PCM player before
507
576
  // its only stream id is lost. Target the exact stream rather than using
@@ -522,21 +591,17 @@ class LocalMiniappRuntime {
522
591
  BgTimer.clearTimeout(existing.authRetryTimerId);
523
592
  existing.authRetryTimerId = null;
524
593
  }
525
- for (const stream of existing.subscriptions) {
526
- const subs = this.streamSubscribers.get(stream);
527
- if (subs) {
528
- subs.delete(packageName);
529
- if (subs.size === 0)
530
- this.streamSubscribers.delete(stream);
531
- }
532
- }
594
+ this.replaceStreamSubscribers(packageName, existing.subscriptions, []);
533
595
  this.recomputeMicRequirements();
534
596
  this.updateCloudSubscriptions();
535
597
  }
536
598
  this.connectedApps.set(packageName, {
537
599
  subscriptions: new Set(),
600
+ forceLocalTranscriptionStreams: new Set(),
601
+ cloudTranscriptionStreams: new Set(),
538
602
  sendMessage: sendFn,
539
603
  lastPongAt: Date.now(),
604
+ unansweredPingRounds: 0,
540
605
  installedManifest,
541
606
  authRefreshTimerId: null,
542
607
  authRetryTimerId: null,
@@ -656,6 +721,7 @@ class LocalMiniappRuntime {
656
721
  }
657
722
  unregisterApp(packageName) {
658
723
  console.log(`${LOG_TAG}: unregisterApp(${packageName})`);
724
+ const releasedMicGateOverride = micStateCoordinator.clearMiniappGateOverrides(packageName);
659
725
  this.clearForegroundProbe(packageName);
660
726
  this.clearMiniappAuthRefresh(packageName);
661
727
  this.clearMiniappAuthDeliveryRetry(packageName);
@@ -664,21 +730,39 @@ class LocalMiniappRuntime {
664
730
  // the early-return so it runs even if the connectedApps entry is already gone.
665
731
  this.handshookApps.delete(packageName);
666
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);
736
+ // Warm-up is a request-owned camera lease. Cancel it even if the app record was already
737
+ // removed so a close racing the camera-open promise cannot leave the sensor running.
738
+ // Keep camera teardown ordered: restoring FOV can restart the HAL, so the warm-up lease must
739
+ // finish closing the sensor before FOV reconciliation begins. Continue to FOV cleanup even if
740
+ // warm-up cancellation fails; phone ownership remains retryable until the ASG lease TTL.
741
+ void phonePhotoCoordinator
742
+ .stopWarmUpForApp(packageName)
743
+ .catch((error) => {
744
+ console.warn(`${LOG_TAG}: failed to stop camera warm-up for ${packageName} on unregister`, error);
745
+ })
746
+ .then(() => phoneCameraFovCoordinator.releaseForApp(packageName))
747
+ .catch((error) => {
748
+ console.warn(`${LOG_TAG}: failed to release camera FOV override for ${packageName} on unregister`, error);
749
+ });
667
750
  const app = this.connectedApps.get(packageName);
668
- if (!app)
669
- return;
670
- // Remove from all stream subscriber sets
671
- for (const stream of app.subscriptions) {
672
- const subs = this.streamSubscribers.get(stream);
673
- if (subs) {
674
- subs.delete(packageName);
675
- if (subs.size === 0) {
676
- this.streamSubscribers.delete(stream);
677
- }
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
+ });
678
756
  }
757
+ return;
679
758
  }
759
+ // Remove from all stream subscriber sets
760
+ this.replaceStreamSubscribers(packageName, app.subscriptions, []);
680
761
  // Reset per-session warning dedup so a relaunch surfaces issues again.
681
762
  resetPermissionWarnings(packageName);
763
+ // Cancel lookahead/in-flight synthesis before stopping the current file so
764
+ // an interrupted sentence cannot advance and reclaim playback.
765
+ this.cancelSpeech(packageName);
682
766
  // Stop audio for this app
683
767
  audioPlaybackService.stopForApp(packageName);
684
768
  // Tear down this app's blob state: abort in-flight uploads + close readers
@@ -728,6 +812,11 @@ class LocalMiniappRuntime {
728
812
  localDisplayManager.onUnmount(packageName);
729
813
  this.connectedApps.delete(packageName);
730
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
+ }
731
820
  this.updateCloudSubscriptions();
732
821
  if (this.connectedApps.size === 0) {
733
822
  this.stopPingLoop();
@@ -824,6 +913,9 @@ class LocalMiniappRuntime {
824
913
  case MiniappRequestType.LOCATION_POLL:
825
914
  this.handleLocationPoll(packageName, requestId);
826
915
  break;
916
+ case MiniappRequestType.CALENDAR_LIST_EVENTS:
917
+ void this.handleCalendarListEvents(packageName, payload, requestId);
918
+ break;
827
919
  case MiniappRequestType.NAVIGATION_START:
828
920
  this.navigationHandlers.handleStart(packageName, payload, requestId);
829
921
  break;
@@ -890,9 +982,18 @@ class LocalMiniappRuntime {
890
982
  case MiniappRequestType.CAMERA_FOV:
891
983
  void this.handleCameraFov(packageName, payload, requestId);
892
984
  break;
985
+ case MiniappRequestType.SET_WIFI_ADB_STATE:
986
+ void this.handleSetWifiAdbState(packageName, payload, requestId);
987
+ break;
893
988
  case MiniappRequestType.IMU_SET_ENABLED:
894
989
  this.handleImuSetEnabled(packageName, payload, requestId);
895
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;
896
997
  case MiniappRequestType.PING:
897
998
  // SDK should handle this itself; reply PONG just in case
898
999
  this.sendToMiniapp(packageName, { type: MiniappResponseType.PONG }, requestId);
@@ -900,23 +1001,6 @@ class LocalMiniappRuntime {
900
1001
  case MiniappResponseType.PONG: {
901
1002
  // Liveness already touched above for every inbound message; the
902
1003
  // PONG carries no other action.
903
- // BGCAP: measure PING→PONG round-trip ONLY here (a real pong), not in
904
- // handlePong (which fires for every inbound envelope). RTT = how long
905
- // the miniapp's own JS engine (Crust serial queue) took to answer — a
906
- // large value means that queue was starved (the transcript→display
907
- // layer). bgcapLastPingSentAt holds the OLDEST outstanding ping (see
908
- // doPingRound), so a multi-round background stall reports its full
909
- // duration on the first pong after resume.
910
- if (BGCAP_TELEMETRY) {
911
- const app = this.connectedApps.get(packageName);
912
- if (app?.bgcapLastPingSentAt != null) {
913
- const rtt = Date.now() - app.bgcapLastPingSentAt;
914
- if (rtt > 1500) {
915
- console.log(`BGCAP: ${packageName} pong rtt=${rtt}ms (miniapp JS engine was starved)`);
916
- }
917
- app.bgcapLastPingSentAt = undefined;
918
- }
919
- }
920
1004
  break;
921
1005
  }
922
1006
  case MiniappRequestType.SHARE:
@@ -931,51 +1015,54 @@ class LocalMiniappRuntime {
931
1015
  case MiniappRequestType.DOWNLOAD:
932
1016
  this.handleDownload(packageName, payload, requestId);
933
1017
  break;
1018
+ case MiniappRequestType.SCAN_QR:
1019
+ void this.handleScanQr(packageName, payload, requestId);
1020
+ break;
934
1021
  // Persistent binary blob storage (session.blob)
935
1022
  case MiniappRequestType.BLOB_CREATE:
936
- this.blobStore.handleCreate(packageName, payload, requestId);
1023
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleCreate(packageName, payload, requestId));
937
1024
  break;
938
1025
  case MiniappRequestType.BLOB_WRITE:
939
- this.blobStore.handleWrite(packageName, payload, requestId);
1026
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleWrite(packageName, payload, requestId));
940
1027
  break;
941
1028
  case MiniappRequestType.BLOB_COMMIT:
942
- this.blobStore.handleCommit(packageName, payload, requestId);
1029
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleCommit(packageName, payload, requestId));
943
1030
  break;
944
1031
  case MiniappRequestType.BLOB_ABORT:
945
- this.blobStore.handleAbort(packageName, payload, requestId);
1032
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleAbort(packageName, payload, requestId));
946
1033
  break;
947
1034
  case MiniappRequestType.BLOB_SET_FROM_URL:
948
- void this.blobStore.handleSetFromUrl(packageName, payload, requestId);
1035
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleSetFromUrl(packageName, payload, requestId));
949
1036
  break;
950
1037
  case MiniappRequestType.BLOB_IMPORT:
951
- void this.blobStore.handleImport(packageName, payload, requestId);
1038
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleImport(packageName, payload, requestId));
952
1039
  break;
953
1040
  case MiniappRequestType.BLOB_GET:
954
- this.blobStore.handleGet(packageName, payload, requestId);
1041
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleGet(packageName, payload, requestId));
955
1042
  break;
956
1043
  case MiniappRequestType.BLOB_LIST:
957
- this.blobStore.handleList(packageName, payload, requestId);
1044
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleList(packageName, payload, requestId));
958
1045
  break;
959
1046
  case MiniappRequestType.BLOB_USAGE:
960
- this.blobStore.handleUsage(packageName, payload, requestId);
1047
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleUsage(packageName, payload, requestId));
961
1048
  break;
962
1049
  case MiniappRequestType.BLOB_DELETE:
963
- this.blobStore.handleDelete(packageName, payload, requestId);
1050
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleDelete(packageName, payload, requestId));
964
1051
  break;
965
1052
  case MiniappRequestType.BLOB_CLEAR:
966
- this.blobStore.handleClear(packageName, payload, requestId);
1053
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleClear(packageName, payload, requestId));
967
1054
  break;
968
1055
  case MiniappRequestType.BLOB_OPEN_READ:
969
- this.blobStore.handleOpenRead(packageName, payload, requestId);
1056
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleOpenRead(packageName, payload, requestId));
970
1057
  break;
971
1058
  case MiniappRequestType.BLOB_READ:
972
- this.blobStore.handleRead(packageName, payload, requestId);
1059
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleRead(packageName, payload, requestId));
973
1060
  break;
974
1061
  case MiniappRequestType.BLOB_CLOSE_READ:
975
- this.blobStore.handleCloseRead(packageName, payload, requestId);
1062
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleCloseRead(packageName, payload, requestId));
976
1063
  break;
977
1064
  case MiniappRequestType.BLOB_SHARE:
978
- void this.blobStore.handleShare(packageName, payload, requestId);
1065
+ this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleShare(packageName, payload, requestId));
979
1066
  break;
980
1067
  // Cloud-coordinated features
981
1068
  case MiniappRequestType.PHOTO:
@@ -1028,8 +1115,19 @@ class LocalMiniappRuntime {
1028
1115
  message: "Dashboard API is deferred in v1",
1029
1116
  });
1030
1117
  break;
1118
+ case MiniappRequestType.TRANSCRIPTION_CONFIG:
1119
+ this.handleTranscriptionConfig(packageName, payload, requestId);
1120
+ break;
1031
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.
1032
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
+ });
1033
1131
  break;
1034
1132
  }
1035
1133
  }
@@ -1046,6 +1144,7 @@ class LocalMiniappRuntime {
1046
1144
  }
1047
1145
  // Update lastPongAt so it doesn't time out right away
1048
1146
  existing.lastPongAt = Date.now();
1147
+ existing.unansweredPingRounds = 0;
1049
1148
  // Read current glasses capabilities from the settings store
1050
1149
  const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable);
1051
1150
  const capabilities = getModelCapabilities(defaultWearable || DeviceTypes.NONE);
@@ -1091,10 +1190,12 @@ class LocalMiniappRuntime {
1091
1190
  this.flushConnectWaiters(packageName);
1092
1191
  }
1093
1192
  async requestMiniappAuth(packageName, opts) {
1094
- const authPackageName = packageName === DEV_APP_PACKAGE_NAME ? getDevAppSourcePackage() : packageName;
1095
- if (!authPackageName)
1096
- return null;
1097
- return cloudClientService.getMiniappAuthToken(authPackageName, opts);
1193
+ const authPackageName = getDevAppSourcePackage(packageName) ?? packageName;
1194
+ const devAttestation = getDevAppAttestation(packageName) ?? undefined;
1195
+ return cloudClientService.getMiniappAuthToken(authPackageName, {
1196
+ ...opts,
1197
+ ...(devAttestation ? { devAttestation } : {}),
1198
+ });
1098
1199
  }
1099
1200
  async handleAuthRefresh(packageName, payload, requestId) {
1100
1201
  try {
@@ -1231,6 +1332,51 @@ class LocalMiniappRuntime {
1231
1332
  this.deliverInitialMiniappAuth(packageName, undefined, 0);
1232
1333
  }
1233
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
+ }
1234
1380
  handleSubscribe(packageName, payload, requestId) {
1235
1381
  const app = this.connectedApps.get(packageName);
1236
1382
  if (!app)
@@ -1241,6 +1387,8 @@ class LocalMiniappRuntime {
1241
1387
  // The strictest rate wins when multiple `location_stream` entries appear
1242
1388
  // in one SUBSCRIBE — the same rule we apply across apps.
1243
1389
  let requestedLocationRate = null;
1390
+ const forceLocalTranscriptionStreams = new Set();
1391
+ const cloudTranscriptionStreams = new Set();
1244
1392
  const streams = rawStreams?.map((s) => {
1245
1393
  if (typeof s === "object" && s !== null) {
1246
1394
  if (s.stream === "location_stream") {
@@ -1249,8 +1397,16 @@ class LocalMiniappRuntime {
1249
1397
  }
1250
1398
  return "location_update";
1251
1399
  }
1400
+ if (s.forceLocal === true && s.stream.startsWith("transcription:")) {
1401
+ forceLocalTranscriptionStreams.add(s.stream);
1402
+ }
1403
+ if (s.stream.startsWith("transcription:") && (s.forceLocal !== true || s.includeCloud === true)) {
1404
+ cloudTranscriptionStreams.add(s.stream);
1405
+ }
1252
1406
  return s.stream;
1253
1407
  }
1408
+ if (s.startsWith("transcription:"))
1409
+ cloudTranscriptionStreams.add(s);
1254
1410
  return s;
1255
1411
  });
1256
1412
  if (!Array.isArray(streams)) {
@@ -1276,8 +1432,6 @@ class LocalMiniappRuntime {
1276
1432
  return "READ_NOTIFICATIONS";
1277
1433
  if (s === "phone_notification_dismissed")
1278
1434
  return "READ_NOTIFICATIONS";
1279
- if (s === "calendar_event")
1280
- return "CALENDAR";
1281
1435
  return null;
1282
1436
  };
1283
1437
  for (const stream of streams) {
@@ -1295,26 +1449,12 @@ class LocalMiniappRuntime {
1295
1449
  return;
1296
1450
  }
1297
1451
  }
1298
- // Remove old subscriptions for this app
1299
- for (const oldStream of app.subscriptions) {
1300
- const subs = this.streamSubscribers.get(oldStream);
1301
- if (subs) {
1302
- subs.delete(packageName);
1303
- if (subs.size === 0) {
1304
- this.streamSubscribers.delete(oldStream);
1305
- }
1306
- }
1307
- }
1308
1452
  // Add new subscriptions
1453
+ const previousSubscriptions = app.subscriptions;
1309
1454
  app.subscriptions = new Set(streams);
1310
- for (const stream of streams) {
1311
- let subs = this.streamSubscribers.get(stream);
1312
- if (!subs) {
1313
- subs = new Set();
1314
- this.streamSubscribers.set(stream, subs);
1315
- }
1316
- subs.add(packageName);
1317
- }
1455
+ app.forceLocalTranscriptionStreams = new Set([...forceLocalTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
1456
+ app.cloudTranscriptionStreams = new Set([...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
1457
+ this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions);
1318
1458
  this.recomputeMicRequirements();
1319
1459
  this.updateCloudSubscriptions();
1320
1460
  this.recomputeHeadingSubscription();
@@ -1329,6 +1469,30 @@ class LocalMiniappRuntime {
1329
1469
  // to wait for the first change event.
1330
1470
  this.emitInitialSnapshots(packageName, streams);
1331
1471
  }
1472
+ async handleCalendarListEvents(packageName, payload, requestId) {
1473
+ const app = this.connectedApps.get(packageName);
1474
+ const declared = app?.installedManifest?.permissions?.some((permission) => permission.type?.toUpperCase() === "CALENDAR");
1475
+ if (!declared) {
1476
+ logPermissionNotDeclared(packageName, "CALENDAR", "to list calendar events", '{"type": "CALENDAR"}');
1477
+ this.sendResult(packageName, requestId, false, undefined, {
1478
+ code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
1479
+ message: 'CALENDAR permission not declared in miniapp.json. Add {"type": "CALENDAR"} to the permissions array.',
1480
+ permission: "CALENDAR",
1481
+ });
1482
+ return;
1483
+ }
1484
+ try {
1485
+ const result = await listPhoneCalendarEvents(payload);
1486
+ this.sendResult(packageName, requestId, true, result);
1487
+ }
1488
+ catch (error) {
1489
+ const known = error instanceof PhoneCalendarError;
1490
+ this.sendResult(packageName, requestId, false, undefined, {
1491
+ code: known ? error.code : MiniappErrorCode.INTERNAL,
1492
+ message: error instanceof Error ? error.message : "Failed to read phone calendar events",
1493
+ });
1494
+ }
1495
+ }
1332
1496
  /**
1333
1497
  * For state-bearing streams (battery, connection), deliver the current value
1334
1498
  * immediately on subscribe. Other streams (button press, transcription, etc.)
@@ -1518,6 +1682,7 @@ class LocalMiniappRuntime {
1518
1682
  const audioRequestId = requestId || `local_${Date.now()}`;
1519
1683
  const volume = typeof payload.volume === "number" ? payload.volume : 1.0;
1520
1684
  const stopOtherAudio = payload.stopOtherAudio !== false;
1685
+ this.cancelSpeech(packageName);
1521
1686
  this.setSpeakerState(packageName, "loading");
1522
1687
  audioPlaybackService.play({ requestId: audioRequestId, audioUrl, appId: packageName, volume, stopOtherAudio }, (_respId, success, error, duration) => {
1523
1688
  if (success) {
@@ -1544,6 +1709,7 @@ class LocalMiniappRuntime {
1544
1709
  queueMicrotask(() => this.setSpeakerState(packageName, "playing"));
1545
1710
  }
1546
1711
  handleStopAudio(packageName, _payload, requestId) {
1712
+ this.cancelSpeech(packageName);
1547
1713
  audioPlaybackService.stopForApp(packageName);
1548
1714
  this.setSpeakerState(packageName, "stopped");
1549
1715
  this.sendResult(packageName, requestId, true);
@@ -1730,84 +1896,184 @@ class LocalMiniappRuntime {
1730
1896
  }
1731
1897
  }
1732
1898
  async handleSpeak(packageName, payload, requestId) {
1733
- const text = payload.text;
1734
- if (!text) {
1899
+ const rawText = payload.text;
1900
+ const rawSentences = typeof rawText === "string"
1901
+ ? [rawText.trim()].filter(Boolean)
1902
+ : Array.isArray(rawText)
1903
+ ? rawText
1904
+ .filter((sentence) => typeof sentence === "string")
1905
+ .map((sentence) => sentence.trim())
1906
+ .filter(Boolean)
1907
+ : [];
1908
+ const enableSanitization = payload.enableSanitization !== false;
1909
+ const sentences = prepareTtsSentences(rawSentences, enableSanitization);
1910
+ if (sentences.length === 0) {
1735
1911
  this.sendResult(packageName, requestId, false, undefined, {
1736
1912
  code: MiniappErrorCode.INTERNAL,
1737
- message: "speak requires text",
1913
+ message: "speak requires text or a non-empty sentence list with speakable content",
1738
1914
  });
1739
1915
  return;
1740
1916
  }
1741
- try {
1742
- const voice = (payload.voice_id ?? payload.voice) || "default";
1743
- const audioRequestId = requestId || `tts_${Date.now()}`;
1744
- const volume = typeof payload.volume === "number" ? payload.volume : 1.0;
1745
- const stopOtherAudio = payload.stopOtherAudio !== false;
1746
- const voiceSettings = payload.voice_settings && typeof payload.voice_settings === "object"
1747
- ? payload.voice_settings
1748
- : undefined;
1749
- const speed = typeof voiceSettings?.speed === "number" ? voiceSettings.speed : undefined;
1750
- this.setSpeakerState(packageName, "loading");
1751
- const voiceExplicit = payload.voice_id !== undefined || payload.voice !== undefined;
1752
- const offlineSupportsVoice = !voiceExplicit || voice === "default" || ttsModelManager.getAvailableLanguages().some((l) => l.code === voice);
1753
- const modelId = typeof payload.model_id === "string" ? payload.model_id : undefined;
1754
- const cloudConnected = cloudClientService.isConnected();
1755
- console.log(`${LOG_TAG}: TTS decision for ${packageName}: cloudConnected=${cloudConnected}, runtimeTts=yes, offlineVoice=${offlineSupportsVoice}`);
1756
- let terminalSent = false;
1757
- const sendPlaybackResult = (success, error, duration, fallbackErrorMessage) => {
1758
- if (terminalSent)
1759
- return;
1760
- terminalSent = true;
1917
+ const cloudText = sentences.join(" ");
1918
+ const voice = (payload.voice_id ?? payload.voice) || "default";
1919
+ const audioRequestId = requestId || `tts_${Date.now()}`;
1920
+ const volume = typeof payload.volume === "number" ? payload.volume : 1.0;
1921
+ const stopOtherAudio = payload.stopOtherAudio !== false;
1922
+ const voiceSettings = payload.voice_settings && typeof payload.voice_settings === "object"
1923
+ ? payload.voice_settings
1924
+ : undefined;
1925
+ const speed = typeof voiceSettings?.speed === "number" ? voiceSettings.speed : undefined;
1926
+ const run = { id: audioRequestId, cancelled: false };
1927
+ this.cancelSpeech(packageName);
1928
+ this.speechRuns.set(packageName, run);
1929
+ this.setSpeakerState(packageName, "loading");
1930
+ let terminalSent = false;
1931
+ const sendPlaybackResult = (success, error, duration, fallbackErrorMessage, completed = success, errorCode = MiniappErrorCode.TTS_UPSTREAM_ERROR) => {
1932
+ if (terminalSent)
1933
+ return;
1934
+ terminalSent = true;
1935
+ // A superseded run still owns its bridge request, but it no longer owns
1936
+ // the package's shared speaker state.
1937
+ const ownsSpeakerState = this.speechRuns.get(packageName) === run;
1938
+ if (ownsSpeakerState)
1939
+ this.speechRuns.delete(packageName);
1940
+ if (ownsSpeakerState) {
1761
1941
  if (success) {
1762
1942
  this.setSpeakerState(packageName, "stopped", { durationMs: duration ?? undefined });
1763
1943
  }
1764
1944
  else {
1765
1945
  this.setSpeakerState(packageName, "error", {
1766
- errorCode: MiniappErrorCode.TTS_UPSTREAM_ERROR,
1946
+ errorCode,
1767
1947
  errorMessage: error ?? fallbackErrorMessage,
1768
1948
  durationMs: duration ?? undefined,
1769
1949
  });
1770
1950
  }
1771
- this.sendResult(packageName, requestId, success, { completed: success, duration }, error
1772
- ? {
1773
- code: MiniappErrorCode.TTS_UPSTREAM_ERROR,
1774
- message: error,
1775
- }
1776
- : undefined);
1777
- };
1951
+ }
1952
+ this.sendResult(packageName, requestId, success, { completed, duration }, error
1953
+ ? {
1954
+ code: errorCode,
1955
+ message: error,
1956
+ }
1957
+ : undefined);
1958
+ };
1959
+ run.onCancelled = () => sendPlaybackResult(true, null, null, "tts playback stopped", false);
1960
+ try {
1961
+ const voiceExplicit = payload.voice_id !== undefined || payload.voice !== undefined;
1962
+ const offlineSupportsVoice = !voiceExplicit || voice === "default" || ttsModelManager.getAvailableLanguages().some((l) => l.code === voice);
1963
+ const modelId = typeof payload.model_id === "string" ? payload.model_id : undefined;
1964
+ const forceLocal = payload.forceLocal === true;
1965
+ const cloudConnected = cloudClientService.isConnected();
1966
+ console.log(`${LOG_TAG}: TTS decision for ${packageName}: cloudConnected=${cloudConnected}, runtimeTts=yes, offlineVoice=${offlineSupportsVoice}, forceLocal=${forceLocal}`);
1778
1967
  const playOfflineTts = async (reason) => {
1968
+ if (run.cancelled)
1969
+ return true;
1779
1970
  if (!offlineSupportsVoice || !(await ttsModelManager.isModelAvailable())) {
1780
1971
  return false;
1781
1972
  }
1973
+ if (run.cancelled)
1974
+ return true;
1975
+ const languageCode = ttsModelManager.getAvailableLanguages().some((l) => l.code === voice) ? voice : undefined;
1976
+ const offlineSentences = sentences;
1977
+ if (offlineSentences.length > 1) {
1978
+ let firstSentence;
1979
+ try {
1980
+ firstSentence = await ttsModelManager.synthesizeToFile(offlineSentences[0], { languageCode, speed });
1981
+ }
1982
+ catch (offlineErr) {
1983
+ if (run.cancelled)
1984
+ return true;
1985
+ console.warn(`${LOG_TAG}: offline sentence TTS synthesize failed${reason ? ` after ${reason}` : ""}:`, offlineErr);
1986
+ return false;
1987
+ }
1988
+ if (run.cancelled) {
1989
+ await Promise.resolve(firstSentence.cleanup?.());
1990
+ return true;
1991
+ }
1992
+ void runSentenceTtsPipeline({
1993
+ sentences: offlineSentences,
1994
+ firstAudio: firstSentence,
1995
+ run,
1996
+ synthesize: (sentence) => ttsModelManager.synthesizeToFile(sentence, { languageCode, speed }),
1997
+ play: (audio, index) => new Promise((resolve) => {
1998
+ const sentenceRequestId = `${audioRequestId}_sentence_${index}`;
1999
+ run.playbackRequestId = sentenceRequestId;
2000
+ audioPlaybackService.play({
2001
+ requestId: sentenceRequestId,
2002
+ audioUrl: audio.audioUrl,
2003
+ appId: packageName,
2004
+ volume,
2005
+ stopOtherAudio,
2006
+ }, (_responseId, success, error, duration, completionReason) => {
2007
+ if (run.playbackRequestId === sentenceRequestId)
2008
+ run.playbackRequestId = undefined;
2009
+ resolve({ success, completed: completionReason === "completed", error, duration });
2010
+ });
2011
+ }),
2012
+ onCleanupError: (cleanupError) => {
2013
+ console.warn(`${LOG_TAG}: sentence TTS cleanup failed`, cleanupError);
2014
+ },
2015
+ }).then(({ success, completed, error, duration }) => {
2016
+ sendPlaybackResult(success, error, duration, "offline sentence tts playback failed", completed);
2017
+ }, (pipelineError) => {
2018
+ const message = pipelineError instanceof Error ? pipelineError.message : String(pipelineError);
2019
+ sendPlaybackResult(false, message, null, "offline sentence tts playback failed", false);
2020
+ });
2021
+ queueMicrotask(() => {
2022
+ if (this.speechRuns.get(packageName) === run)
2023
+ this.setSpeakerState(packageName, "playing");
2024
+ });
2025
+ return true;
2026
+ }
1782
2027
  let offlineGenerated;
1783
2028
  try {
1784
- const languageCode = ttsModelManager.getAvailableLanguages().some((l) => l.code === voice) ? voice : undefined;
1785
- offlineGenerated = await ttsModelManager.synthesizeToFile(text, { languageCode, speed });
2029
+ offlineGenerated = await ttsModelManager.synthesizeToFile(offlineSentences[0], { languageCode, speed });
1786
2030
  }
1787
2031
  catch (offlineErr) {
2032
+ if (run.cancelled)
2033
+ return true;
1788
2034
  console.warn(`${LOG_TAG}: offline TTS synthesize failed${reason ? ` after ${reason}` : ""}:`, offlineErr);
1789
2035
  return false;
1790
2036
  }
1791
2037
  const generated = offlineGenerated;
1792
- audioPlaybackService.play({ requestId: audioRequestId, audioUrl: generated.audioUrl, appId: packageName, volume, stopOtherAudio }, (_respId, success, error, duration) => {
2038
+ if (run.cancelled) {
2039
+ await Promise.resolve(generated.cleanup?.());
2040
+ return true;
2041
+ }
2042
+ run.playbackRequestId = audioRequestId;
2043
+ audioPlaybackService.play({
2044
+ requestId: audioRequestId,
2045
+ audioUrl: generated.audioUrl,
2046
+ appId: packageName,
2047
+ volume,
2048
+ stopOtherAudio,
2049
+ }, (_respId, success, error, duration, completionReason) => {
2050
+ if (run.playbackRequestId === audioRequestId)
2051
+ run.playbackRequestId = undefined;
1793
2052
  void Promise.resolve(generated.cleanup?.()).catch((cleanupError) => {
1794
2053
  console.warn(`${LOG_TAG}: offline TTS cleanup failed`, cleanupError);
1795
2054
  });
1796
- sendPlaybackResult(success, error, duration, "offline tts playback failed");
2055
+ sendPlaybackResult(success, error, duration, "offline tts playback failed", completionReason === "completed");
2056
+ });
2057
+ queueMicrotask(() => {
2058
+ if (this.speechRuns.get(packageName) === run)
2059
+ this.setSpeakerState(packageName, "playing");
1797
2060
  });
1798
- queueMicrotask(() => this.setSpeakerState(packageName, "playing"));
1799
2061
  return true;
1800
2062
  };
1801
2063
  const playCloudTts = async (fallbackToOffline) => {
2064
+ if (run.cancelled)
2065
+ return true;
1802
2066
  let source;
1803
2067
  try {
1804
- source = await cloudClientService.tts.speak(text, {
2068
+ source = await cloudClientService.tts.speak(cloudText, {
1805
2069
  ...(voiceExplicit && voice !== "default" ? { voice_id: voice } : {}),
1806
2070
  ...(modelId ? { model_id: modelId } : {}),
1807
2071
  ...(voiceSettings ? { voice_settings: voiceSettings } : {}),
1808
2072
  });
1809
2073
  }
1810
2074
  catch (cloudErr) {
2075
+ if (run.cancelled)
2076
+ return true;
1811
2077
  const error = cloudErr instanceof Error ? cloudErr.message : String(cloudErr);
1812
2078
  console.warn(`${LOG_TAG}: cloud TTS source failed: ${error}`);
1813
2079
  if (fallbackToOffline && (await playOfflineTts("cloud tts source failed"))) {
@@ -1816,7 +2082,18 @@ class LocalMiniappRuntime {
1816
2082
  sendPlaybackResult(false, error, null, "tts failed");
1817
2083
  return true;
1818
2084
  }
1819
- await Promise.resolve(audioPlaybackService.play({ requestId: audioRequestId, audioUrl: source.audioUrl, appId: packageName, volume, stopOtherAudio }, (_respId, success, error, duration) => {
2085
+ if (run.cancelled)
2086
+ return true;
2087
+ run.playbackRequestId = audioRequestId;
2088
+ await Promise.resolve(audioPlaybackService.play({
2089
+ requestId: audioRequestId,
2090
+ audioUrl: source.audioUrl,
2091
+ appId: packageName,
2092
+ volume,
2093
+ stopOtherAudio,
2094
+ }, (_respId, success, error, duration, completionReason) => {
2095
+ if (run.playbackRequestId === audioRequestId)
2096
+ run.playbackRequestId = undefined;
1820
2097
  if (!success && fallbackToOffline) {
1821
2098
  void playOfflineTts("cloud tts playback failed").then((started) => {
1822
2099
  if (!started) {
@@ -1825,11 +2102,24 @@ class LocalMiniappRuntime {
1825
2102
  });
1826
2103
  return;
1827
2104
  }
1828
- sendPlaybackResult(success, error, duration, "tts failed");
2105
+ sendPlaybackResult(success, error, duration, "tts failed", completionReason === "completed");
1829
2106
  }));
1830
- queueMicrotask(() => this.setSpeakerState(packageName, "playing"));
2107
+ queueMicrotask(() => {
2108
+ if (this.speechRuns.get(packageName) === run)
2109
+ this.setSpeakerState(packageName, "playing");
2110
+ });
1831
2111
  return true;
1832
2112
  };
2113
+ if (forceLocal) {
2114
+ if (await playOfflineTts()) {
2115
+ return;
2116
+ }
2117
+ if (run.cancelled)
2118
+ return;
2119
+ const message = "tts unavailable: forceLocal requested but offline TTS model is not available";
2120
+ sendPlaybackResult(false, message, null, message, false, MiniappErrorCode.TTS_LOCAL_UNAVAILABLE);
2121
+ return;
2122
+ }
1833
2123
  if (cloudConnected && (await playCloudTts(true))) {
1834
2124
  return;
1835
2125
  }
@@ -1842,28 +2132,24 @@ class LocalMiniappRuntime {
1842
2132
  return;
1843
2133
  }
1844
2134
  const message = "tts unavailable: no cloud TTS URL or offline TTS model";
1845
- this.setSpeakerState(packageName, "error", {
1846
- errorCode: MiniappErrorCode.TTS_UPSTREAM_ERROR,
1847
- errorMessage: message,
1848
- });
1849
- this.sendResult(packageName, requestId, false, undefined, {
1850
- code: MiniappErrorCode.TTS_UPSTREAM_ERROR,
1851
- message,
1852
- });
2135
+ sendPlaybackResult(false, message, null, message, false);
1853
2136
  }
1854
2137
  catch (err) {
1855
2138
  console.error(`${LOG_TAG}: speak error:`, err);
1856
2139
  const message = err instanceof Error ? err.message : "TTS error";
1857
- this.setSpeakerState(packageName, "error", {
1858
- errorCode: MiniappErrorCode.TTS_UPSTREAM_ERROR,
1859
- errorMessage: message,
1860
- });
1861
- this.sendResult(packageName, requestId, false, undefined, {
1862
- code: MiniappErrorCode.TTS_UPSTREAM_ERROR,
1863
- message,
1864
- });
2140
+ sendPlaybackResult(false, message, null, "TTS error", false);
1865
2141
  }
1866
2142
  }
2143
+ cancelSpeech(packageName) {
2144
+ const run = this.speechRuns.get(packageName);
2145
+ if (!run)
2146
+ return;
2147
+ run.cancelled = true;
2148
+ this.speechRuns.delete(packageName);
2149
+ if (run.playbackRequestId)
2150
+ audioPlaybackService.cancelPlayback(run.playbackRequestId);
2151
+ run.onCancelled?.();
2152
+ }
1867
2153
  async handleRgbLed(packageName, payload, requestId) {
1868
2154
  const coerceNumber = (value, fallback) => {
1869
2155
  const coerced = Number(value);
@@ -1885,7 +2171,15 @@ class LocalMiniappRuntime {
1885
2171
  }
1886
2172
  async handleLocationPoll(packageName, requestId) {
1887
2173
  try {
1888
- 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`);
1889
2183
  if (status !== "granted") {
1890
2184
  this.sendResult(packageName, requestId, false, undefined, {
1891
2185
  code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
@@ -1897,8 +2191,11 @@ class LocalMiniappRuntime {
1897
2191
  // a fresh fix if the cache is empty. Use Low accuracy on the fresh
1898
2192
  // path so we get a cell/wifi-tower fix in ~1s instead of waiting
1899
2193
  // for full GPS warm-up.
2194
+ const locationStartedAt = Date.now();
1900
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`);
1901
2197
  const location = cached ?? (await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Low }));
2198
+ console.log(`${LOG_TAG}: location poll resolved elapsed=${Date.now() - locationStartedAt}ms`);
1902
2199
  this.sendResult(packageName, requestId, true, {
1903
2200
  lat: location.coords.latitude,
1904
2201
  lng: location.coords.longitude,
@@ -1933,8 +2230,64 @@ class LocalMiniappRuntime {
1933
2230
  */
1934
2231
  blobStore = new BlobStore({
1935
2232
  sendResult: (packageName, requestId, ok, result, error) => this.sendResult(packageName, requestId, ok, result, error),
1936
- 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(),
1937
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
+ }
1938
2291
  /**
1939
2292
  * Heading is a sensor stream — start the native compass when any mini
1940
2293
  * app is subscribed to "heading_update", stop it when none are.
@@ -2009,10 +2362,6 @@ class LocalMiniappRuntime {
2009
2362
  // ---------------------------------------------------------------------------
2010
2363
  // Storage helpers
2011
2364
  // ---------------------------------------------------------------------------
2012
- getStorageKeyPrefix(packageName) {
2013
- const userId = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.coreToken) || "anonymous";
2014
- return `mentraos_localstorage_${userId}_${packageName}_`;
2015
- }
2016
2365
  async handleStorageGet(packageName, payload, requestId) {
2017
2366
  try {
2018
2367
  const key = payload.key;
@@ -2023,9 +2372,8 @@ class LocalMiniappRuntime {
2023
2372
  });
2024
2373
  return;
2025
2374
  }
2026
- const fullKey = this.getStorageKeyPrefix(packageName) + key;
2027
- const result = mmkvStorage.load(fullKey);
2028
- 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 });
2029
2377
  }
2030
2378
  catch (err) {
2031
2379
  console.error(`${LOG_TAG}: storage_get error:`, err);
@@ -2045,8 +2393,7 @@ class LocalMiniappRuntime {
2045
2393
  });
2046
2394
  return;
2047
2395
  }
2048
- const fullKey = this.getStorageKeyPrefix(packageName) + key;
2049
- mmkvStorage.save(fullKey, payload.value ?? null);
2396
+ await this.simpleStorage.set(packageName, key, payload.value ?? null);
2050
2397
  this.sendResult(packageName, requestId, true);
2051
2398
  }
2052
2399
  catch (err) {
@@ -2067,8 +2414,7 @@ class LocalMiniappRuntime {
2067
2414
  });
2068
2415
  return;
2069
2416
  }
2070
- const fullKey = this.getStorageKeyPrefix(packageName) + key;
2071
- mmkvStorage.remove(fullKey);
2417
+ await this.simpleStorage.delete(packageName, key);
2072
2418
  this.sendResult(packageName, requestId, true);
2073
2419
  }
2074
2420
  catch (err) {
@@ -2081,9 +2427,7 @@ class LocalMiniappRuntime {
2081
2427
  }
2082
2428
  async handleStorageList(packageName, _payload, requestId) {
2083
2429
  try {
2084
- const prefix = this.getStorageKeyPrefix(packageName);
2085
- const allKeys = mmkvStorage.getAllKeys();
2086
- const keys = allKeys.filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
2430
+ const keys = await this.simpleStorage.keys(packageName);
2087
2431
  this.sendResult(packageName, requestId, true, { keys });
2088
2432
  }
2089
2433
  catch (err) {
@@ -2096,12 +2440,7 @@ class LocalMiniappRuntime {
2096
2440
  }
2097
2441
  async handleStorageClear(packageName, requestId) {
2098
2442
  try {
2099
- const prefix = this.getStorageKeyPrefix(packageName);
2100
- const allKeys = mmkvStorage.getAllKeys();
2101
- for (const k of allKeys) {
2102
- if (k.startsWith(prefix))
2103
- mmkvStorage.remove(k);
2104
- }
2443
+ await this.simpleStorage.clear(packageName);
2105
2444
  this.sendResult(packageName, requestId, true);
2106
2445
  }
2107
2446
  catch (err) {
@@ -2122,9 +2461,8 @@ class LocalMiniappRuntime {
2122
2461
  });
2123
2462
  return;
2124
2463
  }
2125
- const fullKey = this.getStorageKeyPrefix(packageName) + key;
2126
- const result = mmkvStorage.load(fullKey);
2127
- 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 });
2128
2466
  }
2129
2467
  catch (err) {
2130
2468
  console.error(`${LOG_TAG}: storage_has error:`, err);
@@ -2136,18 +2474,7 @@ class LocalMiniappRuntime {
2136
2474
  }
2137
2475
  async handleStorageGetAll(packageName, requestId) {
2138
2476
  try {
2139
- const prefix = this.getStorageKeyPrefix(packageName);
2140
- const allKeys = mmkvStorage.getAllKeys();
2141
- const values = {};
2142
- for (const k of allKeys) {
2143
- if (!k.startsWith(prefix))
2144
- continue;
2145
- const r = mmkvStorage.load(k);
2146
- if (r.is_ok()) {
2147
- const v = r.value;
2148
- values[k.slice(prefix.length)] = typeof v === "string" ? v : String(v ?? "");
2149
- }
2150
- }
2477
+ const values = await this.simpleStorage.getAll(packageName);
2151
2478
  this.sendResult(packageName, requestId, true, { values });
2152
2479
  }
2153
2480
  catch (err) {
@@ -2168,10 +2495,7 @@ class LocalMiniappRuntime {
2168
2495
  });
2169
2496
  return;
2170
2497
  }
2171
- const prefix = this.getStorageKeyPrefix(packageName);
2172
- for (const [key, value] of Object.entries(values)) {
2173
- mmkvStorage.save(prefix + key, value ?? null);
2174
- }
2498
+ await this.simpleStorage.setMultiple(packageName, values);
2175
2499
  this.sendResult(packageName, requestId, true);
2176
2500
  }
2177
2501
  catch (err) {
@@ -2183,6 +2507,29 @@ class LocalMiniappRuntime {
2183
2507
  }
2184
2508
  }
2185
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
+ // ---------------------------------------------------------------------------
2186
2533
  // Camera FOV
2187
2534
  // ---------------------------------------------------------------------------
2188
2535
  async handleCameraFov(packageName, payload, requestId) {
@@ -2202,12 +2549,7 @@ class LocalMiniappRuntime {
2202
2549
  const request = normalizeCameraFovPayload(payload);
2203
2550
  const description = "preset" in request ? `preset=${request.preset}` : `fov=${request.fov} roi=${request.roiPosition ?? "center"}`;
2204
2551
  console.log(`${LOG_TAG}: camera_fov_set ${description}`);
2205
- // Camera FOV is a direct btsdk call now (was the host-injected `cameraSettings`
2206
- // runtime hook — a pure BluetoothSdk.setCameraFov passthrough with no host coupling).
2207
- const result = await BluetoothSdk.setCameraFov(request);
2208
- useSettingsStore
2209
- .getState()
2210
- .setSetting(ISLAND_SETTINGS_KEYS.cameraFov, { fov: result.fov, roi_position: CAMERA_ROI_POSITION_VALUES[result.roiPosition] }, false);
2552
+ const result = await phoneCameraFovCoordinator.setOverride(packageName, request);
2211
2553
  this.sendResult(packageName, requestId, true, result);
2212
2554
  }
2213
2555
  catch (err) {
@@ -2244,6 +2586,103 @@ class LocalMiniappRuntime {
2244
2586
  });
2245
2587
  }
2246
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
+ }
2247
2686
  // ---------------------------------------------------------------------------
2248
2687
  // System utilities (share, open URL, clipboard, download)
2249
2688
  // ---------------------------------------------------------------------------
@@ -2371,10 +2810,20 @@ class LocalMiniappRuntime {
2371
2810
  const result = await phonePhotoCoordinator.takePhoto(packageName, {
2372
2811
  size: payload.size,
2373
2812
  mode: payload.mode,
2813
+ transferMethod: payload.transferMethod,
2374
2814
  compress: payload.compress,
2375
2815
  sound: payload.sound,
2376
2816
  saveToGallery: payload.saveToGallery,
2377
2817
  exposureTimeNs: payload.exposureTimeNs,
2818
+ iso: payload.iso,
2819
+ aeExposureDivisor: payload.aeExposureDivisor,
2820
+ isoCap: payload.isoCap,
2821
+ noiseReduction: payload.noiseReduction,
2822
+ edgeEnhancement: payload.edgeEnhancement,
2823
+ zsl: payload.zsl,
2824
+ mfnr: payload.mfnr,
2825
+ ispDigitalGain: payload.ispDigitalGain,
2826
+ ispAnalogGain: payload.ispAnalogGain,
2378
2827
  });
2379
2828
  this.sendResult(packageName, requestId, true, result);
2380
2829
  }
@@ -2404,8 +2853,11 @@ class LocalMiniappRuntime {
2404
2853
  try {
2405
2854
  await phonePhotoCoordinator.warmUpCamera(packageName, {
2406
2855
  size: payload.size,
2856
+ mode: payload.mode,
2407
2857
  exposureTimeNs: payload.exposureTimeNs,
2408
2858
  durationMs: payload.durationMs,
2859
+ zsl: payload.zsl,
2860
+ mfnr: payload.mfnr,
2409
2861
  });
2410
2862
  this.sendResult(packageName, requestId, true);
2411
2863
  }
@@ -2485,6 +2937,7 @@ class LocalMiniappRuntime {
2485
2937
  video: normalizeStreamVideoConfig(payload.video),
2486
2938
  audio: normalizeStreamAudioConfig(payload.audio),
2487
2939
  sound: payload.sound,
2940
+ authToken: typeof payload.authToken === "string" ? payload.authToken : undefined,
2488
2941
  });
2489
2942
  this.sendResult(packageName, requestId, true, result);
2490
2943
  }
@@ -2563,7 +3016,7 @@ class LocalMiniappRuntime {
2563
3016
  return;
2564
3017
  }
2565
3018
  try {
2566
- await requestWifiSetup(payload.reason);
3019
+ await requestWifiSetup(payload.reason, packageName);
2567
3020
  this.sendResult(packageName, requestId, true);
2568
3021
  }
2569
3022
  catch (err) {
@@ -2622,26 +3075,12 @@ class LocalMiniappRuntime {
2622
3075
  return { ok: false, error: "MICROPHONE permission not declared in miniapp.json" };
2623
3076
  }
2624
3077
  }
2625
- // Remove old subscriptions for this app
2626
- for (const oldStream of app.subscriptions) {
2627
- const subs = this.streamSubscribers.get(oldStream);
2628
- if (subs) {
2629
- subs.delete(packageName);
2630
- if (subs.size === 0) {
2631
- this.streamSubscribers.delete(oldStream);
2632
- }
2633
- }
2634
- }
2635
3078
  // Add new subscriptions
3079
+ const previousSubscriptions = app.subscriptions;
2636
3080
  app.subscriptions = new Set(streams);
2637
- for (const stream of streams) {
2638
- let subs = this.streamSubscribers.get(stream);
2639
- if (!subs) {
2640
- subs = new Set();
2641
- this.streamSubscribers.set(stream, subs);
2642
- }
2643
- subs.add(packageName);
2644
- }
3081
+ app.forceLocalTranscriptionStreams.clear();
3082
+ app.cloudTranscriptionStreams = new Set(streams.filter((stream) => stream.startsWith("transcription:")));
3083
+ this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions);
2645
3084
  this.recomputeMicRequirements();
2646
3085
  this.updateCloudSubscriptions();
2647
3086
  return { ok: true };
@@ -2649,6 +3088,15 @@ class LocalMiniappRuntime {
2649
3088
  // ===========================================================================
2650
3089
  // Mic requirements
2651
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
+ }
2652
3100
  recomputeMicRequirements() {
2653
3101
  let anyPcm = false;
2654
3102
  let anyLc3 = false;
@@ -2660,7 +3108,11 @@ class LocalMiniappRuntime {
2660
3108
  if (stream.startsWith("transcription:") || stream.startsWith("translation:") || stream === "vad")
2661
3109
  anyLc3 = true;
2662
3110
  }
2663
- micStateCoordinator.setLocalRequirements({ pcm: anyPcm, lc3: anyLc3 });
3111
+ micStateCoordinator.setLocalRequirements({
3112
+ pcm: anyPcm,
3113
+ lc3: anyLc3,
3114
+ ...this.getConfiguredMicGates(),
3115
+ });
2664
3116
  }
2665
3117
  /**
2666
3118
  * Recompute the aggregated v2 cloud subscription list across all local miniapps.
@@ -2672,20 +3124,31 @@ class LocalMiniappRuntime {
2672
3124
  updateCloudSubscriptions() {
2673
3125
  const cloudStreams = new Set();
2674
3126
  let transcriptionLang = null;
3127
+ let forceLocalTranscriptionLang = null;
3128
+ let hasForceLocalTranscription = false;
2675
3129
  for (const [stream, subscribers] of this.streamSubscribers) {
2676
3130
  if (subscribers.size === 0)
2677
3131
  continue;
2678
- // Only transcription / translation need cloud delivery. Location,
2679
- // notifications, and calendar events are sourced natively on the phone
2680
- // and forwarded to miniapps directly via MantleManager — no cloud hop.
2681
- if (stream.startsWith("transcription:") || stream.startsWith("translation:")) {
3132
+ // Only transcription / translation need cloud delivery. Location and
3133
+ // notifications are sourced natively on the phone — no cloud hop.
3134
+ if (stream.startsWith("translation:")) {
2682
3135
  cloudStreams.add(stream);
2683
3136
  }
2684
- if (stream.startsWith("transcription:") && transcriptionLang === null) {
2685
- transcriptionLang = stream.substring("transcription:".length);
3137
+ if (stream.startsWith("transcription:")) {
3138
+ const language = stream.substring("transcription:".length);
3139
+ if (transcriptionLang === null)
3140
+ transcriptionLang = language;
3141
+ const routes = summarizeTranscriptionRoutes(subscribers, (packageName) => this.appTranscriptionRoutesForSubscription(packageName, stream));
3142
+ if (routes.hasForceLocalSubscriber) {
3143
+ hasForceLocalTranscription = true;
3144
+ if (forceLocalTranscriptionLang === null)
3145
+ forceLocalTranscriptionLang = language;
3146
+ }
3147
+ if (routes.hasCloudSubscriber)
3148
+ cloudStreams.add(stream);
2686
3149
  }
2687
3150
  }
2688
- localSttFallbackCoordinator.onSubscriptionChange(transcriptionLang !== null, transcriptionLang);
3151
+ localSttFallbackCoordinator.onSubscriptionChange(transcriptionLang !== null, forceLocalTranscriptionLang ?? transcriptionLang, hasForceLocalTranscription);
2689
3152
  // Mirror the same set as typed AudioSubscription[] and push it to the cloud
2690
3153
  // runtime.
2691
3154
  this.ensureCloudResultsWired();
@@ -2720,11 +3183,20 @@ class LocalMiniappRuntime {
2720
3183
  */
2721
3184
  buildCloudAudioSubscriptions(cloudStreams) {
2722
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();
2723
3191
  const langSource = (code) => code === "auto" || code === "*" ? { mode: "auto" } : { mode: "specific", code };
2724
3192
  for (const stream of cloudStreams) {
2725
3193
  if (stream.startsWith("transcription:")) {
2726
3194
  const lang = stream.substring("transcription:".length);
2727
- 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
+ });
2728
3200
  }
2729
3201
  else if (stream.startsWith("translation:")) {
2730
3202
  const parts = stream.split(":");
@@ -2787,7 +3259,7 @@ class LocalMiniappRuntime {
2787
3259
  provider: d.provider,
2788
3260
  confidence: d.confidence,
2789
3261
  __hostReceivedAt: receivedAt,
2790
- });
3262
+ }, "cloud");
2791
3263
  });
2792
3264
  cloudClientService.onTranslation((d) => {
2793
3265
  this.forwardEvent(`translation:${d.source.language}:${d.target.language}`, {
@@ -2858,7 +3330,7 @@ class LocalMiniappRuntime {
2858
3330
  * - Cloud sends "head_up" → miniapp protocol uses "head_position" (HEAD_POSITION)
2859
3331
  * - Cloud sends "VAD" (uppercase) → miniapp protocol uses "vad" (lowercase)
2860
3332
  */
2861
- forwardEvent(streamType, data) {
3333
+ forwardEvent(streamType, data, transcriptionSource) {
2862
3334
  // Translate cloud event names to miniapp protocol stream types
2863
3335
  const normalizedStream = this.normalizeStreamType(streamType);
2864
3336
  // Collect all subscribers: exact match, plus wildcard matches for streams
@@ -2938,15 +3410,15 @@ class LocalMiniappRuntime {
2938
3410
  }
2939
3411
  if (matchedSubs.size === 0 && !perGestureStream)
2940
3412
  return;
2941
- // BGCAP: count transcripts only once we know they're actually being handed
2942
- // to a subscriber. If the captions miniapp is unregistered/respawning (or
2943
- // hasn't subscribed yet) the cloud may still push transcripts that match no
2944
- // one — counting those would falsely show "forwarded" during the exact gap
2945
- // the diagnostic is meant to expose.
2946
- if (BGCAP_TELEMETRY && normalizedStream.startsWith("transcription:")) {
2947
- bgcapNoteTranscriptForward(!!data?.isFinal);
2948
- }
2949
3413
  for (const packageName of matchedSubs) {
3414
+ let transcriptionRoute;
3415
+ if (normalizedStream.startsWith("transcription:") && transcriptionSource) {
3416
+ const routes = this.appTranscriptionRoutesForEvent(packageName, normalizedStream);
3417
+ transcriptionRoute =
3418
+ transcriptionDeliveryRoute(transcriptionSource, routes, cloudClientService.isConnected()) ?? undefined;
3419
+ if (!transcriptionRoute)
3420
+ continue;
3421
+ }
2950
3422
  if (TRANSCRIPT_TIMING_TELEMETRY && normalizedStream.startsWith("transcription:")) {
2951
3423
  const transcript = data;
2952
3424
  const now = Date.now();
@@ -2964,6 +3436,7 @@ class LocalMiniappRuntime {
2964
3436
  type: MiniappResponseType.EVENT,
2965
3437
  streamType: normalizedStream,
2966
3438
  data: outboundData,
3439
+ ...(transcriptionRoute ? { transcriptionRoute } : {}),
2967
3440
  });
2968
3441
  }
2969
3442
  // Send a separate envelope to per-gesture subscribers tagged with the
@@ -2981,6 +3454,22 @@ class LocalMiniappRuntime {
2981
3454
  }
2982
3455
  }
2983
3456
  }
3457
+ appTranscriptionRoutesForSubscription(packageName, stream) {
3458
+ const app = this.connectedApps.get(packageName);
3459
+ return {
3460
+ cloud: app?.cloudTranscriptionStreams.has(stream) === true,
3461
+ forceLocal: app?.forceLocalTranscriptionStreams.has(stream) === true,
3462
+ };
3463
+ }
3464
+ appTranscriptionRoutesForEvent(packageName, stream) {
3465
+ const app = this.connectedApps.get(packageName);
3466
+ const autoStream = `${MiniappStreamType.TRANSCRIPTION}:auto`;
3467
+ return {
3468
+ cloud: app?.cloudTranscriptionStreams.has(stream) === true || app?.cloudTranscriptionStreams.has(autoStream) === true,
3469
+ forceLocal: app?.forceLocalTranscriptionStreams.has(stream) === true ||
3470
+ app?.forceLocalTranscriptionStreams.has(autoStream) === true,
3471
+ };
3472
+ }
2984
3473
  /**
2985
3474
  * Translate cloud event names to miniapp stream type values.
2986
3475
  */
@@ -3469,7 +3958,6 @@ class LocalMiniappRuntime {
3469
3958
  // Ping / pong liveness
3470
3959
  // ===========================================================================
3471
3960
  ensurePingLoop() {
3472
- bgcapStartHeartbeat(); // BGCAP diagnostic
3473
3961
  if (this.pingIntervalId !== null)
3474
3962
  return;
3475
3963
  this.pingIntervalId = BgTimer.setInterval(() => {
@@ -3477,28 +3965,22 @@ class LocalMiniappRuntime {
3477
3965
  }, PING_INTERVAL_MS);
3478
3966
  }
3479
3967
  stopPingLoop() {
3480
- bgcapStopHeartbeat(); // BGCAP diagnostic
3481
3968
  if (this.pingIntervalId !== null) {
3482
3969
  BgTimer.clearInterval(this.pingIntervalId);
3483
3970
  this.pingIntervalId = null;
3484
3971
  }
3485
3972
  }
3486
3973
  doPingRound() {
3487
- const now = Date.now();
3488
- const staleThreshold = PING_INTERVAL_MS * PING_TIMEOUT_THRESHOLD;
3489
3974
  const toRemove = [];
3490
3975
  for (const [packageName, app] of this.connectedApps) {
3491
- if (now - app.lastPongAt > staleThreshold) {
3976
+ const liveness = advanceMiniappPingLiveness(app.unansweredPingRounds, PING_TIMEOUT_THRESHOLD);
3977
+ if (liveness.shouldUnregister) {
3492
3978
  console.warn(`${LOG_TAG}: ${packageName} missed ${PING_TIMEOUT_THRESHOLD} pings, unregistering`);
3493
3979
  toRemove.push(packageName);
3494
3980
  continue;
3495
3981
  }
3982
+ app.unansweredPingRounds = liveness.unansweredPingRounds;
3496
3983
  // Send PING — SDK auto-replies with PONG
3497
- // BGCAP: stamp the OLDEST outstanding ping only — don't overwrite, or a
3498
- // background stall (pings keep firing on the native BgTimer while the
3499
- // miniapp engine is frozen) would reset the clock and hide its duration.
3500
- if (BGCAP_TELEMETRY && app.bgcapLastPingSentAt == null)
3501
- app.bgcapLastPingSentAt = now;
3502
3984
  this.sendToMiniapp(packageName, {
3503
3985
  type: MiniappRequestType.PING,
3504
3986
  });
@@ -3518,6 +4000,7 @@ class LocalMiniappRuntime {
3518
4000
  const app = this.connectedApps.get(packageName);
3519
4001
  if (app) {
3520
4002
  app.lastPongAt = Date.now();
4003
+ app.unansweredPingRounds = 0;
3521
4004
  this.clearForegroundProbe(packageName);
3522
4005
  }
3523
4006
  }
@@ -3541,7 +4024,18 @@ class LocalMiniappRuntime {
3541
4024
  }
3542
4025
  // Belt-and-suspenders: clear any remaining state
3543
4026
  this.pendingCloudRequests.clear();
4027
+ const hadButtonPressSubscribers = this.getButtonPressSubscribers().length > 0;
3544
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
+ }
3545
4039
  this.connectedApps.clear();
3546
4040
  for (const timerId of this.foregroundProbeTimers.values()) {
3547
4041
  BgTimer.clearTimeout(timerId);