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

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
@@ -2,8 +2,8 @@
2
2
  * OtaInstallCoordinator — engine-owned OTA install state machine (WP 8B).
3
3
  *
4
4
  * Owns every timer/retry/sequencing rule that used to live in the host screen
5
- * `mobile/src/app/ota/progress.tsx`: the global session timeout, the no-ack
6
- * retry watchdog, the stuck-at-zero watchdog, the progress-stall watchdog keyed
5
+ * `mobile/src/app/ota/progress.tsx`: the global session timeout, serialized
6
+ * ota_start retries, the stuck-at-zero watchdog, the progress-stall watchdog keyed
7
7
  * on a staleness signature, connect-edge arbitration (initial mount vs
8
8
  * reconnect vs post-APK reboot), the ota_query_status reply fallback, the ping
9
9
  * keepalive, the ota_start_ack / mtk_update_complete listeners, and terminal
@@ -11,20 +11,19 @@
11
11
  * `snapshot()`/`onSnapshot()` plus the `attach()/detach()/retry()/finish()`
12
12
  * commands (exposed as `engine.ota.installSession`).
13
13
  *
14
- * Behavior contract: everything here was MOVED, not changed every timer
15
- * duration (see ./otaInstallPolicy), arbitration rule, and "[OTA_PROGRESS]"
16
- * log line is a field-debugging contract and stays byte-identical to the old
17
- * screen. The reaction pass mirrors the old component's effect ordering: a
14
+ * The reaction pass mirrors the old component's effect ordering: a
18
15
  * pass diffs the store-derived inputs (connected / otaStatus / otaProgress /
19
16
  * displayState / stall signature) exactly like the old effect dep arrays, and
20
17
  * state written mid-pass queues a follow-up pass — the same way a setState
21
18
  * during React effects scheduled a re-render after the current effect batch.
22
19
  */
23
20
  import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
24
- import type {OtaProgress, OtaStatus} from "@mentra/bluetooth-sdk/internal"
21
+ import type {OtaProgress, OtaStatus} from "@mentra/bluetooth-sdk"
25
22
  import GlobalEventEmitter from "../utils/GlobalEventEmitter"
26
23
  import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
27
- import {getAsgOtaVersionUrl} from "./asgOtaVersionUrl"
24
+ import {resolveOtaManifestUrl} from "./otaManifestUrl"
25
+ import {hotspotOtaTransport, type HotspotOtaPhase} from "./HotspotOtaTransport"
26
+ import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
28
27
  import {deriveDisplayState, type DisplayState} from "./otaDisplayState"
29
28
  import {
30
29
  BES_CONTINUE_LOCKOUT_MS,
@@ -48,18 +47,41 @@ import {
48
47
  MTK_INSTALL_TIMEOUT_MS,
49
48
  OtaProgressMessages,
50
49
  PING_INTERVAL_MS,
51
- POST_APK_OTA_START_DELAY_MS,
52
50
  PROGRESS_TIMEOUT_MS,
53
51
  QUERY_REPLY_TIMEOUT_MS,
54
52
  RETRY_INTERVAL_MS,
55
53
  isLegacyShapedOtaSession,
56
54
  isLegacyShapedOtaStatus,
55
+ selectOtaProtocolProfile,
56
+ type OtaProtocolProfile,
57
57
  } from "./otaInstallPolicy"
58
58
 
59
59
  function isTerminalForWatchdog(d: DisplayState): boolean {
60
60
  return d === "complete" || d === "failed" || d === "restarting"
61
61
  }
62
62
 
63
+ function hotspotPreflightErrorMessage(error: unknown): string {
64
+ const code =
65
+ typeof error === "object" && error !== null && "code" in error ? String((error as {code: unknown}).code) : ""
66
+ switch (code) {
67
+ case "artifact_download_failed":
68
+ return OtaProgressMessages.hotspotArtifactDownloadFailed
69
+ case "artifact_verify_failed":
70
+ case "manifest_invalid":
71
+ return OtaProgressMessages.hotspotArtifactVerifyFailed
72
+ case "hotspot_wifi_permission_denied":
73
+ return OtaProgressMessages.hotspotWifiPermissionDenied
74
+ case "hotspot_start_failed":
75
+ return OtaProgressMessages.hotspotStartFailed
76
+ case "hotspot_join_failed":
77
+ return OtaProgressMessages.hotspotJoinFailed
78
+ case "hotspot_server_failed":
79
+ return OtaProgressMessages.hotspotServerFailed
80
+ default:
81
+ return OtaProgressMessages.hotspotServerFailed
82
+ }
83
+ }
84
+
63
85
  /** Non-empty when glasses are actively reporting work — drives stall timeout reset. */
64
86
  function buildProgressStalenessSignature(
65
87
  otaStatus: OtaStatus | null,
@@ -134,10 +156,36 @@ export interface OtaInstallSnapshot {
134
156
  * +1% ticks up to 60% purely to reassure the user. Render max(real, simulated).
135
157
  */
136
158
  mtkInstallStallSimulatedPercent: number | null
159
+ /** True for a downgrade (version-change) session — the UI narrates it differently. */
160
+ isVersionChange: boolean
161
+ /** True once the glasses reported the exact pinned target (the downgrade truly completed). */
162
+ versionChangeConverged: boolean
163
+ /**
164
+ * Sub-phase of the downgrade detour for the progress narrative, or null when not
165
+ * in one: "installing" before the handoff, "restarting" while the glasses are dark
166
+ * (uninstall / factory flicker / target install), "verifying" once reconnected and
167
+ * checking the version. Completion is the "complete" displayState.
168
+ */
169
+ versionChangePhase: "installing" | "restarting" | "verifying" | null
170
+ /** Pre-ota_start phone staging/join state for a hotspot attempt. */
171
+ hotspotPhase: HotspotOtaPhase
172
+ hotspotArtifactPercent: number | null
173
+ }
174
+
175
+ type OtaStartOutcome = "pending" | "acknowledged" | "rejected"
176
+
177
+ interface OtaStartOwnership {
178
+ outcome: OtaStartOutcome
179
+ promise: Promise<void>
137
180
  }
138
181
 
139
182
  class OtaInstallCoordinator {
140
183
  private attached = false
184
+ private preparedCheckResult: OtaCheckCurrentGlassesResult | null = null
185
+ private selectedTransport: "wifi" | "hotspot" = "wifi"
186
+ private hotspotManifestUrl: string | null = null
187
+ private hotspotPhase: HotspotOtaPhase = "idle"
188
+ private hotspotArtifactPercent: number | null = null
141
189
 
142
190
  // Genuinely session-local state (was component state/refs).
143
191
  private errorMsg = ""
@@ -157,6 +205,30 @@ class OtaInstallCoordinator {
157
205
  private apkExpectedInSession = false
158
206
  // Latched when the legacy build-number completion fires; projected as "complete".
159
207
  private apkCompletedViaBuildIncrease = false
208
+ // The split routes selected legacy/unified behavior before navigation. Keep
209
+ // that choice sticky while an APK step upgrades ASG mid-flow.
210
+ private protocolProfile: OtaProtocolProfile | null = null
211
+
212
+ // BES reboot recovery. Native bridges normalize explicit BES success to a
213
+ // terminal status; pre-37 flows additionally retain the legacy screen's
214
+ // lost-FINISHED fallback. Completion still requires a BES-specific observed
215
+ // disconnect -> reconnect lifecycle, never the generic session reconnect flag.
216
+ private besRestartExpected = false
217
+ private besRestartDisconnectObserved = false
218
+ private besRestartRecovery: "awaiting" | "complete" | null = null
219
+
220
+ // Downgrade detour (version-change mode). Captured at attach from the selected
221
+ // update: the pinned target is a LOWER build, executed by the recovery worker via
222
+ // uninstall->reinstall, so the phone must NOT drive ota_start on the intermediate
223
+ // reconnect (the passive factory build) and completion is exact-version
224
+ // convergence, not a build-number increase.
225
+ private versionChangeSession = false
226
+ private versionChangeTarget: number | null = null
227
+ // Set once ASG reports the apk install has started (the handoff point after which
228
+ // ASG dies and the recovery worker owns the transaction).
229
+ private versionChangeInstallStarted = false
230
+ // Latched when a reconnect reports buildNumber === versionChangeTarget.
231
+ private versionChangeConverged = false
160
232
  // Holds a legacy apk install FINISHED out of "complete" for the settle window.
161
233
  private legacyApkSettleHold = false
162
234
  // Display-only legacy MTK install stall simulation.
@@ -169,7 +241,6 @@ class OtaInstallCoordinator {
169
241
  private retryTimeout: ReturnType<typeof setTimeout> | null = null
170
242
  private stuckTimeout: ReturnType<typeof setTimeout> | null = null
171
243
  private progressTimeout: ReturnType<typeof setTimeout> | null = null
172
- private postApkDelay: ReturnType<typeof setTimeout> | null = null
173
244
  private pingInterval: ReturnType<typeof setInterval> | null = null
174
245
  private queryReplyTimeout: ReturnType<typeof setTimeout> | null = null
175
246
  private continueLockoutTimer: ReturnType<typeof setTimeout> | null = null
@@ -177,8 +248,14 @@ class OtaInstallCoordinator {
177
248
  private mtkStallDetectTimer: ReturnType<typeof setTimeout> | null = null
178
249
  private mtkSimTickTimer: ReturnType<typeof setInterval> | null = null
179
250
 
180
- // Retry / ack bookkeeping (no glasses source)
181
- private hasReceivedAck = false
251
+ // Request / retry bookkeeping (no glasses source)
252
+ // At most one apk-install status poll in flight (native rejects concurrent queries).
253
+ private apkInstallPollInFlight = false
254
+ // The native SDK owns one ota_start request until its application-level ack or
255
+ // timeout. Keep pending ownership across attach/detach, and retain the settled
256
+ // outcome while attached so Retry/query fallback cannot reinterpret an accepted
257
+ // command as permission to start a second OTA transaction.
258
+ private otaStartOwnership: OtaStartOwnership | null = null
182
259
  private hasFirstActivity = false
183
260
  // Stuck-at-zero watchdog clears only on first NON-ZERO progress; "first activity"
184
261
  // alone (e.g. ota_status with stepPercent=0) used to clear it too eagerly.
@@ -206,6 +283,25 @@ class OtaInstallCoordinator {
206
283
 
207
284
  // --- public surface (engine.ota.installSession) ---
208
285
 
286
+ /** Select the transport at the existing install entry point before the progress route attaches. */
287
+ prepare(checkResult: OtaCheckCurrentGlassesResult): "wifi" | "hotspot" {
288
+ const state = useGlassesStore.getState()
289
+ if (!state.wifiStatusKnown) {
290
+ throw new Error("Glasses Wi-Fi status is not available")
291
+ }
292
+ const hotspotSupported = state.hotspotOtaVersion === 1
293
+ const wifiConnected = state.wifi.state === "connected"
294
+ if (!wifiConnected && !hotspotSupported) {
295
+ throw new Error("Connected glasses require Wi-Fi for OTA")
296
+ }
297
+ this.preparedCheckResult = checkResult
298
+ this.selectedTransport = wifiConnected ? "wifi" : "hotspot"
299
+ this.hotspotManifestUrl = null
300
+ this.hotspotPhase = "idle"
301
+ this.hotspotArtifactPercent = null
302
+ return this.selectedTransport
303
+ }
304
+
209
305
  /**
210
306
  * Bind the state machine to a mounted progress screen. Idempotent per
211
307
  * attach/detach cycle. Runs the initial-mount arbitration (send ota_start
@@ -216,6 +312,18 @@ class OtaInstallCoordinator {
216
312
  if (this.attached) return
217
313
  this.attached = true
218
314
  this.resetSessionState()
315
+ const initialState = useGlassesStore.getState()
316
+ this.protocolProfile = selectOtaProtocolProfile(
317
+ initialState.otaStatus,
318
+ initialState.otaProgress,
319
+ initialState.buildNumber,
320
+ )
321
+ const selected = initialState.otaUpdateAvailable
322
+ if (selected?.isDowngrade && typeof selected.versionCode === "number" && selected.versionCode > 0) {
323
+ this.versionChangeSession = true
324
+ this.versionChangeTarget = selected.versionCode
325
+ console.log(`[OTA_PROGRESS] version-change (downgrade) session, target=${selected.versionCode}`)
326
+ }
219
327
  this.prevConnected = isGlassesConnected(useGlassesStore.getState().connection)
220
328
  this.storeUnsubscribe = useGlassesStore.subscribe(() => this.react())
221
329
  // Mount pass: every reaction block runs once (like all effects on mount),
@@ -246,14 +354,67 @@ class OtaInstallCoordinator {
246
354
  }
247
355
  GlobalEventEmitter.off("ota_start_ack", this.handleAck)
248
356
  GlobalEventEmitter.off("mtk_update_complete", this.handleMtkComplete)
357
+ GlobalEventEmitter.off("glasses_session_changed", this.handleGlassesSessionChanged)
249
358
  this.clearAllOtaTimers()
250
359
  this.clearContinueLockoutTimer()
360
+ // Only native command ownership survives a remount. Once the command has
361
+ // settled, the glasses store/durable ota_status is the session authority.
362
+ if (this.otaStartOwnership?.outcome !== "pending") {
363
+ this.otaStartOwnership = null
364
+ }
251
365
  this.resetSessionState()
252
366
  }
253
367
 
254
368
  /** Retry after a failure: clear state and re-send ota_start (if connected). */
255
369
  retry(): void {
256
- console.log("[OTA_PROGRESS] retry pressed, clearing state and re-sending ota_start")
370
+ // While the recovery worker owns the detour (apk install latched, not yet
371
+ // converged), the phone must not drive: an ota_start now would push the
372
+ // factory/surviving build into a parallel download-install, and a second
373
+ // handoff would begin()/REPLACE the live transaction under the running
374
+ // worker. Retry just re-enters the reconcile wait — convergence or the
375
+ // session timeout decides the real outcome.
376
+ if (this.isInVersionChangeDetour()) {
377
+ console.log("[OTA_PROGRESS] retry during version-change detour — re-entering wait (no ota_start)")
378
+ this.setErrorMsg("")
379
+ this.emitInternalChange()
380
+ return
381
+ }
382
+ const store = useGlassesStore.getState()
383
+ const otaStatus = store.otaStatus
384
+ const otaProgress = store.otaProgress
385
+ const authoritativeFailure = otaStatus?.status === "failed" || otaProgress?.status === "FAILED"
386
+ const hasUnfinishedSession =
387
+ (!!otaStatus &&
388
+ otaStatus.status !== "idle" &&
389
+ otaStatus.status !== "failed" &&
390
+ otaStatus.status !== "complete") ||
391
+ (!!otaProgress && otaProgress.status !== "FAILED" && otaProgress.status !== "FINISHED")
392
+ const shouldReconcile =
393
+ this.otaStartOwnership?.outcome === "pending" ||
394
+ (!authoritativeFailure &&
395
+ (this.otaStartOwnership?.outcome === "acknowledged" || this.hasFirstActivity || hasUnfinishedSession))
396
+
397
+ if (shouldReconcile) {
398
+ console.log("[OTA_PROGRESS] retry pressed for an existing OTA session — reconciling without ota_start")
399
+ this.clearPerStepTimers()
400
+ this.clearLegacyApkSettleTimer()
401
+ this.clearMtkSimulationTimers()
402
+ this.clearContinueLockoutTimer()
403
+ this.setErrorMsg("")
404
+ this.maybeStartGlobalTimeout()
405
+ this.maybeArmStuckWatchdog()
406
+ if (isGlassesConnected(store.connection)) {
407
+ if (this.otaStartOwnership?.outcome === "pending") {
408
+ void this.sendOtaStartWithWatchdogs()
409
+ } else {
410
+ void BluetoothSdk.sendOtaQueryStatus().catch(() => {})
411
+ this.armQueryReplyFallback("retry")
412
+ }
413
+ }
414
+ return
415
+ }
416
+
417
+ console.log("[OTA_PROGRESS] retry pressed after a rejected or terminal attempt — starting a fresh ota_start")
257
418
  // Batch like the old event handler: React ran the whole handler (including
258
419
  // the ota_start send) before re-rendering + re-running effects.
259
420
  const wasReacting = this.reacting
@@ -262,16 +423,16 @@ class OtaInstallCoordinator {
262
423
  this.clearPerStepTimers()
263
424
  this.clearLegacyApkSettleTimer()
264
425
  this.clearMtkSimulationTimers()
426
+ this.clearContinueLockoutTimer()
265
427
  this.retryCount = 0
266
- this.hasFirstActivity = false
267
- this.hasReceivedAck = false
428
+ if (!this.prepareFreshOtaStartAttempt()) return
429
+ this.resetBesRestartAttempt()
268
430
  this.setSawReconnectEdge(false)
269
431
  this.setErrorMsg("")
270
- this.setApkCompletedViaBuildIncrease(false)
432
+ this.setContinueButtonDisabled(false)
271
433
  this.setLegacyApkSettleHold(false)
272
434
  this.setMtkSimulatedPercent(null)
273
435
  this.lastRealMtkProgress = 0
274
- const store = useGlassesStore.getState()
275
436
  store.setOtaProgress(null)
276
437
  store.setOtaStatus(null)
277
438
  if (isGlassesConnected(useGlassesStore.getState().connection)) {
@@ -289,11 +450,15 @@ class OtaInstallCoordinator {
289
450
  * clear the stale build number so the next check re-reads version_info.
290
451
  */
291
452
  finish(): void {
453
+ if (this.otaStartOwnership?.outcome !== "pending") {
454
+ this.otaStartOwnership = null
455
+ }
292
456
  useGlassesStore.getState().setOtaUpdateAvailable(null)
293
457
  if (this.apkStepSeen) {
294
458
  BluetoothSdk.updateGlasses({buildNumber: ""})
295
459
  useGlassesStore.getState().setGlassesInfo({buildNumber: ""})
296
460
  }
461
+ void this.teardownHotspotTransport()
297
462
  }
298
463
 
299
464
  /**
@@ -323,17 +488,44 @@ class OtaInstallCoordinator {
323
488
  sawReconnectEdge: this.sawReconnectEdge,
324
489
  legacyApkSettleHold: this.legacyApkSettleHold,
325
490
  apkCompletedViaBuildIncrease: this.apkCompletedViaBuildIncrease,
491
+ besRestartRecovery: this.besRestartRecovery,
492
+ versionChangeConverged: this.versionChangeConverged,
493
+ versionChangeSession: this.versionChangeSession,
326
494
  }),
327
495
  errorMsg: this.errorMsg,
328
- continueButtonDisabled: this.continueButtonDisabled,
496
+ // Keep Continue disabled while the expected BES reboot is still in flight.
497
+ continueButtonDisabled: this.continueButtonDisabled || this.besRestartRecovery === "awaiting",
329
498
  connected,
330
499
  // Copies: the snapshot must not hand callers mutable references into the store.
331
500
  otaStatus: otaStatus ? {...otaStatus} : null,
332
501
  otaProgress: otaProgress ? {...otaProgress} : null,
333
502
  mtkInstallStallSimulatedPercent: this.mtkSimulatedPercent,
503
+ isVersionChange: this.versionChangeSession,
504
+ versionChangeConverged: this.versionChangeConverged,
505
+ versionChangePhase: this.deriveVersionChangePhase(connected),
506
+ hotspotPhase: this.hotspotPhase,
507
+ hotspotArtifactPercent: this.hotspotArtifactPercent,
334
508
  }
335
509
  }
336
510
 
511
+ /**
512
+ * True while the recovery worker owns the downgrade transaction (after the apk install
513
+ * started, before convergence). During this window ASG is being uninstalled/reinstalled and
514
+ * cannot answer the phone, so the coordinator must not ping, poll ota_query_status, or arm the
515
+ * progress-stall watchdog — doing so drives the passive factory build and fails the session
516
+ * before the target reconnects. Completion is exact-version convergence, detected separately.
517
+ */
518
+ private isInVersionChangeDetour(): boolean {
519
+ return this.versionChangeSession && this.versionChangeInstallStarted && !this.versionChangeConverged
520
+ }
521
+
522
+ /** Downgrade-detour sub-phase for the progress narrative (see OtaInstallSnapshot). */
523
+ private deriveVersionChangePhase(connected: boolean): "installing" | "restarting" | "verifying" | null {
524
+ if (!this.versionChangeSession || this.versionChangeConverged) return null
525
+ if (!this.versionChangeInstallStarted) return "installing"
526
+ return connected ? "verifying" : "restarting"
527
+ }
528
+
337
529
  /**
338
530
  * Subscribe to install snapshot changes, deduped on the projected JSON.
339
531
  * Fires on glasses-store changes AND coordinator-internal state changes
@@ -367,10 +559,16 @@ class OtaInstallCoordinator {
367
559
  this.initialBuildNumber = null
368
560
  this.apkExpectedInSession = false
369
561
  this.apkCompletedViaBuildIncrease = false
562
+ this.protocolProfile = null
563
+ this.resetBesRestartAttempt()
564
+ this.versionChangeSession = false
565
+ this.versionChangeTarget = null
566
+ this.versionChangeInstallStarted = false
567
+ this.versionChangeConverged = false
370
568
  this.legacyApkSettleHold = false
371
569
  this.mtkSimulatedPercent = null
372
570
  this.lastRealMtkProgress = 0
373
- this.hasReceivedAck = false
571
+ this.apkInstallPollInFlight = false
374
572
  this.hasFirstActivity = false
375
573
  this.hasFirstNonZeroProgress = false
376
574
  this.retryCount = 0
@@ -385,6 +583,26 @@ class OtaInstallCoordinator {
385
583
  this.reactQueued = false
386
584
  }
387
585
 
586
+ /** Begin a genuinely new command attempt only after native ownership has settled. */
587
+ private prepareFreshOtaStartAttempt(): boolean {
588
+ if (this.otaStartOwnership?.outcome === "pending") {
589
+ console.error("[OTA_PROGRESS] refused fresh ota_start while native still owns the previous request")
590
+ return false
591
+ }
592
+ this.otaStartOwnership = null
593
+ this.hasFirstActivity = false
594
+ this.hasFirstNonZeroProgress = false
595
+ this.setApkCompletedViaBuildIncrease(false)
596
+ return true
597
+ }
598
+
599
+ /** Reset only the reboot lifecycle that belongs to one install attempt. */
600
+ private resetBesRestartAttempt(): void {
601
+ this.besRestartExpected = false
602
+ this.besRestartDisconnectObserved = false
603
+ this.besRestartRecovery = null
604
+ }
605
+
388
606
  /** Notify snapshot subscribers of a coordinator-internal state change. */
389
607
  private emitInternalChange(): void {
390
608
  for (const check of Array.from(this.snapshotCheckers)) check()
@@ -434,18 +652,21 @@ class OtaInstallCoordinator {
434
652
  this.emitInternalChange()
435
653
  }
436
654
 
437
- /**
438
- * Legacy-session shape (WP 8C): event shape wins once events exist; before any event
439
- * only the session-start build number can tell (falls back to the live build number
440
- * until it is captured). Old builds get the LEGACY_* padded policy durations.
441
- */
655
+ /** Legacy/unified policy is selected once, matching the old sticky route choice. */
442
656
  private isLegacySessionShapeNow(): boolean {
657
+ if (this.protocolProfile) return this.protocolProfile === "legacy"
443
658
  const state = useGlassesStore.getState()
444
- return isLegacyShapedOtaSession(
659
+ const selected = selectOtaProtocolProfile(
445
660
  state.otaStatus,
446
661
  state.otaProgress,
447
662
  this.initialBuildNumber || state.buildNumber,
448
663
  )
664
+ if (selected) {
665
+ this.protocolProfile = selected
666
+ console.log(`[OTA_PROGRESS] protocol profile selected: ${selected}`)
667
+ return selected === "legacy"
668
+ }
669
+ return isLegacyShapedOtaSession(state.otaStatus, state.otaProgress, state.buildNumber)
449
670
  }
450
671
 
451
672
  private react(): void {
@@ -498,6 +719,19 @@ class OtaInstallCoordinator {
498
719
 
499
720
  const legacySession = this.isLegacySessionShapeNow()
500
721
 
722
+ if (this.isBesRestartExpected(otaStatus, otaProgress, legacySession)) {
723
+ this.besRestartExpected = true
724
+ }
725
+
726
+ // Port the legacy route's actual reboot invariant before deriving display
727
+ // state, so reconnecting with stale BES in_progress@100 never renders updating.
728
+ if (connectedChanged && !connected && this.besRestartExpected && this.besRestartRecovery === null) {
729
+ this.besRestartDisconnectObserved = true
730
+ this.besRestartRecovery = "awaiting"
731
+ console.log("[OTA_PROGRESS] BES restart disconnect observed — awaiting reconnect verification")
732
+ }
733
+ if (this.besRestartRecovery === "awaiting" && connected) this.completeBesRestartRecovery()
734
+
501
735
  // Derived UI state — the glasses data IS the source of truth.
502
736
  const displayState = deriveDisplayState({
503
737
  otaStatus,
@@ -507,6 +741,9 @@ class OtaInstallCoordinator {
507
741
  sawReconnectEdge: this.sawReconnectEdge,
508
742
  legacyApkSettleHold: this.legacyApkSettleHold,
509
743
  apkCompletedViaBuildIncrease: this.apkCompletedViaBuildIncrease,
744
+ besRestartRecovery: this.besRestartRecovery,
745
+ versionChangeConverged: this.versionChangeConverged,
746
+ versionChangeSession: this.versionChangeSession,
510
747
  })
511
748
  const stallSig = buildProgressStalenessSignature(otaStatus, otaProgress, displayState)
512
749
  const stallDuration = progressTimeoutDurationMs(otaStatus, otaProgress, legacySession)
@@ -572,6 +809,73 @@ class OtaInstallCoordinator {
572
809
  useGlassesStore.getState().setMtkUpdatedThisSession(true)
573
810
  }
574
811
 
812
+ // Version-change (downgrade) install started: once ASG reports the apk install
813
+ // phase, the handoff to the recovery worker has happened. From here the phone
814
+ // stops driving ota_start (see runConnectEdge) and waits for exact-version
815
+ // convergence — the wipe means no resumable ASG session and no build increase.
816
+ if (
817
+ this.versionChangeSession &&
818
+ !this.versionChangeInstallStarted &&
819
+ // Only latch from a status that belongs to THIS session: an install/apk status is a
820
+ // response to our own acknowledged ota_start, so a leftover from a prior session
821
+ // sitting in the store at mount cannot prematurely enter the detour wait (which would
822
+ // suppress ota_start and hang the fresh downgrade until the global timeout).
823
+ this.otaStartOwnership?.outcome === "acknowledged" &&
824
+ otaStatus?.stepType === "apk" &&
825
+ otaStatus.phase === "install"
826
+ ) {
827
+ console.log("[OTA_PROGRESS] version-change: apk install started — entering detour wait")
828
+ this.versionChangeInstallStarted = true
829
+ this.emitInternalChange()
830
+ }
831
+
832
+ // Ownership refuted: ASG emits install/STARTED BEFORE handing off, so the latch above can
833
+ // be set even when no transaction ends up owning the detour. Recovery answers every
834
+ // handoff synchronously, so exactly three error codes prove non-ownership and release the
835
+ // latch: downgrade_handoff_refused (recovery's explicit verdict),
836
+ // downgrade_handoff_failed (no verdict at all — recovery dead/missing, so it never began),
837
+ // and downgrade_transaction_stalled (the long-stop past recovery's own stale give-up).
838
+ // An accepted-but-slow transaction emits NONE of these (acceptance cancels the short
839
+ // watchdog), so the latch is never released while a live worker owns the staged artifact
840
+ // — which recovery additionally claims by rename at acceptance.
841
+ if (
842
+ this.versionChangeInstallStarted &&
843
+ !this.versionChangeConverged &&
844
+ otaStatus?.status === "failed" &&
845
+ (otaStatus.error === "downgrade_handoff_refused" ||
846
+ otaStatus.error === "downgrade_handoff_failed" ||
847
+ otaStatus.error === "downgrade_transaction_stalled")
848
+ ) {
849
+ console.log(
850
+ `[OTA_PROGRESS] version-change: no transaction owns the detour (${otaStatus.error}) — releasing latch`,
851
+ )
852
+ this.versionChangeInstallStarted = false
853
+ this.emitInternalChange()
854
+ }
855
+
856
+ // Version-change convergence: the reconnected glasses report exactly the pinned
857
+ // target. This is the ONLY completion signal for a downgrade (lower build, no
858
+ // increase, wiped session). Mirrors the legacy build-increase latch but by equality.
859
+ if (
860
+ this.versionChangeSession &&
861
+ this.versionChangeInstallStarted &&
862
+ !this.versionChangeConverged &&
863
+ this.versionChangeTarget !== null &&
864
+ buildNumberChanged &&
865
+ buildNumber
866
+ ) {
867
+ const currentBuild = Number.parseInt(buildNumber, 10)
868
+ if (Number.isFinite(currentBuild) && currentBuild === this.versionChangeTarget) {
869
+ console.log(`[OTA_PROGRESS] version-change converged: glasses report target ${currentBuild}, completing`)
870
+ this.clearPerStepTimers()
871
+ this.clearGlobalTimeout()
872
+ this.apkStepSeen = true
873
+ this.setErrorMsg("")
874
+ this.versionChangeConverged = true
875
+ this.emitInternalChange()
876
+ }
877
+ }
878
+
575
879
  // Legacy APK completion by build-number increase (WP 8C-c): old builds reboot into
576
880
  // the new build without a reliable explicit reconnect status; the fresh version_info
577
881
  // build number is the completion signal (ported from progress-legacy.tsx, which
@@ -614,7 +918,7 @@ class OtaInstallCoordinator {
614
918
 
615
919
  /**
616
920
  * Connect-edge — the only place that decides to send ota_start /
617
- * ota_query_status, handles POST_APK delay, and flips sawReconnectEdge
921
+ * ota_query_status and flips sawReconnectEdge
618
922
  * on the false -> true transition that signals the BES reboot completed.
619
923
  */
620
924
  if (connectedChanged) {
@@ -628,23 +932,26 @@ class OtaInstallCoordinator {
628
932
  if (isMount) {
629
933
  GlobalEventEmitter.on("ota_start_ack", this.handleAck)
630
934
  GlobalEventEmitter.on("mtk_update_complete", this.handleMtkComplete)
935
+ GlobalEventEmitter.on("glasses_session_changed", this.handleGlassesSessionChanged)
631
936
  }
632
937
 
633
938
  // Ping keepalive while an OTA is actively running (legacy sessions pinged on the
634
939
  // padded interval — old builds slept less eagerly and pings are costlier for them).
635
940
  if (connectedChanged || displayStateChanged) {
636
941
  this.clearPingInterval()
637
- const active = connected && (displayState === "starting" || displayState === "updating")
942
+ const active =
943
+ connected && (displayState === "starting" || displayState === "updating") && !this.isInVersionChangeDetour()
638
944
  if (active) {
639
945
  void BluetoothSdk.ping().catch(() => {})
640
946
  const pingIntervalMs = legacySession ? LEGACY_PING_INTERVAL_MS : PING_INTERVAL_MS
641
947
  this.pingInterval = setInterval(() => {
642
948
  void BluetoothSdk.ping().catch(() => {})
949
+ this.maybePollApkInstallStatus()
643
950
  }, pingIntervalMs)
644
951
  }
645
952
  }
646
953
 
647
- // Any glasses activity (ota_status or otaProgress) clears the no-ack retry watchdog.
954
+ // Any glasses activity proves the logical ota_start was accepted.
648
955
  if ((otaStatusChanged || otaProgressChanged) && (otaStatus || otaProgress)) {
649
956
  this.onFirstActivity()
650
957
  }
@@ -667,7 +974,10 @@ class OtaInstallCoordinator {
667
974
  // update that doesn't change the signature keeps the same timer running
668
975
  // instead of forever re-extending the deadline.
669
976
  if (stallSigChanged || displayStateChanged) {
670
- if (displayState !== "starting" && displayState !== "updating") {
977
+ if (this.isInVersionChangeDetour()) {
978
+ // Recovery worker owns the transaction; there are no progress events to stall on.
979
+ this.clearProgressTimeout()
980
+ } else if (displayState !== "starting" && displayState !== "updating") {
671
981
  this.clearProgressTimeout()
672
982
  } else if (!stallSig) {
673
983
  this.clearProgressTimeout()
@@ -685,20 +995,14 @@ class OtaInstallCoordinator {
685
995
 
686
996
  // Lockout on Continue button after BES restart to prevent accidental tap
687
997
  // (15s unified, 35s for legacy-shaped sessions — WP 8C-f).
688
- if (displayStateChanged) {
998
+ if (displayStateChanged && displayState === "restarting") {
689
999
  this.clearContinueLockoutTimer()
690
- if (displayState === "restarting") {
691
- const lockoutMs = legacySession ? LEGACY_BES_CONTINUE_LOCKOUT_MS : BES_CONTINUE_LOCKOUT_MS
692
- this.setContinueButtonDisabled(true)
693
- this.continueLockoutTimer = setTimeout(() => {
694
- this.continueLockoutTimer = null
695
- this.setContinueButtonDisabled(false)
696
- }, lockoutMs)
697
- }
698
- }
699
-
700
- if (displayStateChanged && displayState === "failed") {
701
- this.clearPerStepTimers()
1000
+ const lockoutMs = legacySession ? LEGACY_BES_CONTINUE_LOCKOUT_MS : BES_CONTINUE_LOCKOUT_MS
1001
+ this.setContinueButtonDisabled(true)
1002
+ this.continueLockoutTimer = setTimeout(() => {
1003
+ this.continueLockoutTimer = null
1004
+ this.setContinueButtonDisabled(false)
1005
+ }, lockoutMs)
702
1006
  }
703
1007
 
704
1008
  // Legacy MTK install stall simulation (WP 8C-e): display-only. The simulation
@@ -716,8 +1020,54 @@ class OtaInstallCoordinator {
716
1020
  displayStateChanged &&
717
1021
  (displayState === "complete" || displayState === "restarting" || displayState === "failed")
718
1022
  ) {
1023
+ if (displayState === "failed") {
1024
+ this.clearPerStepTimers()
1025
+ }
719
1026
  useGlassesStore.getState().setOtaUpdateAvailable(null)
1027
+ if (displayState === "complete" || displayState === "failed") {
1028
+ void this.teardownHotspotTransport()
1029
+ }
1030
+ }
1031
+ }
1032
+
1033
+ /**
1034
+ * Reconnect arbitration shared by the physical BLE connect edge and the
1035
+ * glasses_session_changed signal. The latter exists because the BES keeps the BLE
1036
+ * link alive across asg_client restarts (APK OTA): no physical edge ever fires, so
1037
+ * the changed process session id is the only reconnect signal the coordinator gets
1038
+ * (incident rep_01KY6BJ0B7A4RBMQ7VN39KAE5E). Always returns true (the caller's
1039
+ * edge handling is complete).
1040
+ */
1041
+ private runReconnectArbitration(label: string): boolean {
1042
+ console.log(`[OTA_PROGRESS] ${label}: false->true, flipping sawReconnectEdge=true`)
1043
+ // A physical reconnect passed through the disconnect branch first, which cleared
1044
+ // these timers; a session-change edge never disconnects, so a fallback armed by an
1045
+ // earlier mount/query could otherwise fire alongside the arbitration below and send
1046
+ // a duplicate ota_start. Clearing here is a no-op on the physical path.
1047
+ this.clearQueryReplyTimeout()
1048
+ this.setSawReconnectEdge(true)
1049
+
1050
+ if (this.besRestartDisconnectObserved) {
1051
+ console.log(`[OTA_PROGRESS] ${label}: BES reboot reconnect observed — no ota_query_status`)
1052
+ this.completeBesRestartRecovery()
1053
+ return true
1054
+ }
1055
+
1056
+ // Legacy apk settle window (WP 8C): the glasses restarting into the new build is
1057
+ // the EXPECTED reconnect here — the legacy screen took no action on it. Skip the
1058
+ // query/fallback; the settle timer (or the build-number increase) completes.
1059
+ if (this.legacyApkSettleHold) {
1060
+ console.log(`[OTA_PROGRESS] ${label}: reconnect during legacy apk settle hold — no query`)
1061
+ return true
1062
+ }
1063
+
1064
+ console.log(`[OTA_PROGRESS] ${label}: reconnected, sending ota_query_status`)
1065
+ if (this.otaStartOwnership?.outcome === "pending") {
1066
+ void this.sendOtaStartWithWatchdogs()
720
1067
  }
1068
+ void BluetoothSdk.sendOtaQueryStatus()
1069
+ this.armQueryReplyFallback("reconnect")
1070
+ return true
721
1071
  }
722
1072
 
723
1073
  private runConnectEdge(connected: boolean): void {
@@ -726,54 +1076,30 @@ class OtaInstallCoordinator {
726
1076
 
727
1077
  if (!connected) {
728
1078
  console.log("[OTA_PROGRESS] connect-edge: disconnected")
729
- this.clearPostApkDelay()
730
- // Also drop a pending query-reply fallback: firing ota_start into a dead
731
- // link can only produce a false "failed" state, and the reconnect edge
732
- // re-queries and re-arms this fallback anyway.
1079
+ // Also drop pending query/retry sends: firing ota_start into a dead link
1080
+ // can only produce a false "failed" state. Reconnect arbitration first
1081
+ // queries durable status and re-arms the one permitted fallback.
733
1082
  this.clearQueryReplyTimeout()
1083
+ this.clearRetryTimeout()
734
1084
  return
735
1085
  }
736
1086
 
737
1087
  const becameConnected = prev === false && connected === true
738
- if (becameConnected) {
739
- console.log("[OTA_PROGRESS] connect-edge: false->true, flipping sawReconnectEdge=true")
740
- this.setSawReconnectEdge(true)
741
- }
742
-
743
- // Legacy apk settle window (WP 8C): the glasses restarting into the new build is
744
- // the EXPECTED reconnect here — the legacy screen took no action on it. Skip the
745
- // query/fallback; the settle timer (or the build-number increase) completes.
746
- if (becameConnected && this.legacyApkSettleHold) {
747
- console.log("[OTA_PROGRESS] connect-edge: reconnect during legacy apk settle hold — no query")
1088
+ if (becameConnected && this.runReconnectArbitration("connect-edge")) {
748
1089
  return
749
1090
  }
750
1091
 
751
- const storeState = useGlassesStore.getState()
752
- const s = storeState.otaStatus
753
- const postApkAwaiting =
754
- s?.stepType === "apk" && (s?.totalSteps ?? 0) > 1 && (s.status === "step_complete" || s.status === "complete")
755
-
756
- if (becameConnected && postApkAwaiting) {
757
- console.log("[OTA_PROGRESS] connect-edge: post-APK reboot detected, arming POST_APK delay for sendOtaStart")
758
- this.clearPostApkDelay()
759
- this.postApkDelay = setTimeout(() => {
760
- this.postApkDelay = null
761
- this.retryCount = 0
762
- this.hasFirstActivity = false
763
- this.hasFirstNonZeroProgress = false
764
- this.hasReceivedAck = false
765
- console.log("[OTA_PROGRESS] POST_APK delay fired, sending ota_start")
766
- void this.sendOtaStartWithWatchdogs()
767
- }, POST_APK_OTA_START_DELAY_MS)
1092
+ // Version-change detour: after the install started, EVERY reconnect is expected
1093
+ // (the passive factory build, then the target). Do NOT send ota_start/query — the
1094
+ // factory build would treat the pin as a plain upgrade and start a redundant
1095
+ // download racing the recovery worker. Convergence (buildNumber === target) is
1096
+ // detected in the reaction pass; here we simply take no action.
1097
+ if (this.versionChangeSession && this.versionChangeInstallStarted && !this.versionChangeConverged) {
1098
+ console.log("[OTA_PROGRESS] connect-edge: reconnect during version-change detour no query (expected)")
768
1099
  return
769
1100
  }
770
1101
 
771
- if (becameConnected) {
772
- console.log("[OTA_PROGRESS] connect-edge: reconnected, sending ota_query_status")
773
- void BluetoothSdk.sendOtaQueryStatus()
774
- this.armQueryReplyFallback("reconnect")
775
- return
776
- }
1102
+ const storeState = useGlassesStore.getState()
777
1103
 
778
1104
  // Initial mount (prev === current === true). If no session yet, kick off ota_start.
779
1105
  // An idle ota_status has an empty sessionId; treat it as no session so ota_start
@@ -781,15 +1107,21 @@ class OtaInstallCoordinator {
781
1107
  const isIdleStatus = !!storeState.otaStatus && storeState.otaStatus.sessionId === ""
782
1108
  const noSessionYet = (!storeState.otaStatus && !storeState.otaProgress) || isIdleStatus
783
1109
  if (noSessionYet) {
1110
+ if (!isIdleStatus && this.otaStartOwnership?.outcome === "acknowledged") {
1111
+ console.log("[OTA_PROGRESS] initial mount, acknowledged session has no cached status — reconciling")
1112
+ void BluetoothSdk.sendOtaQueryStatus().catch(() => {})
1113
+ this.armQueryReplyFallback("initial-mount")
1114
+ return
1115
+ }
784
1116
  console.log(
785
1117
  isIdleStatus
786
1118
  ? "[OTA_PROGRESS] initial mount, idle status (no sessionId), sending ota_start"
787
1119
  : "[OTA_PROGRESS] initial mount, no session in store, sending ota_start",
788
1120
  )
789
1121
  this.retryCount = 0
790
- this.hasFirstActivity = false
791
- this.hasFirstNonZeroProgress = false
792
- this.hasReceivedAck = false
1122
+ if (isIdleStatus && this.otaStartOwnership?.outcome !== "pending" && !this.prepareFreshOtaStartAttempt()) {
1123
+ return
1124
+ }
793
1125
  void this.sendOtaStartWithWatchdogs()
794
1126
  } else {
795
1127
  console.log("[OTA_PROGRESS] initial mount, session exists, sending ota_query_status")
@@ -801,12 +1133,22 @@ class OtaInstallCoordinator {
801
1133
  // --- BLE OTA event listeners (engine GlobalEventEmitter, fed by OtaService) ---
802
1134
 
803
1135
  private readonly handleAck = (): void => {
804
- if (this.hasReceivedAck) return
1136
+ const ownership = this.otaStartOwnership
1137
+ if (!ownership || ownership.outcome === "rejected") return
1138
+ if (ownership.outcome === "acknowledged") return
805
1139
  console.log("[OTA_PROGRESS] ota_start_ack received")
806
- this.hasReceivedAck = true
1140
+ ownership.outcome = "acknowledged"
807
1141
  this.clearRetryTimeout()
808
1142
  }
809
1143
 
1144
+ /**
1145
+ * The asg process restarted while the BES kept the BLE link up (sid change observed
1146
+ * by the bridge): treat it exactly like a physical reconnect edge.
1147
+ */
1148
+ private readonly handleGlassesSessionChanged = (): void => {
1149
+ this.runReconnectArbitration("session-change")
1150
+ }
1151
+
810
1152
  private readonly handleMtkComplete = (): void => {
811
1153
  console.log("[OTA_PROGRESS] mtk_update_complete received")
812
1154
  this.clearProgressTimeout()
@@ -834,7 +1176,9 @@ class OtaInstallCoordinator {
834
1176
  }
835
1177
 
836
1178
  if (this.errorMsg) {
837
- console.log("[OTA_PROGRESS] legacy install complete arrived after a failure — completing (legacy last-write-wins)")
1179
+ console.log(
1180
+ "[OTA_PROGRESS] legacy install complete arrived after a failure — completing (legacy last-write-wins)",
1181
+ )
838
1182
  this.setErrorMsg("")
839
1183
  this.clearLegacyApkSettleTimer()
840
1184
  this.setLegacyApkSettleHold(false)
@@ -860,6 +1204,43 @@ class OtaInstallCoordinator {
860
1204
  }
861
1205
  }
862
1206
 
1207
+ private isBesRestartExpected(
1208
+ otaStatus: OtaStatus | null,
1209
+ otaProgress: OtaProgress | null,
1210
+ legacySession: boolean,
1211
+ ): boolean {
1212
+ const statusReadyToRestart =
1213
+ otaStatus?.stepType === "bes" &&
1214
+ otaStatus.phase === "install" &&
1215
+ (otaStatus.status === "step_complete" || otaStatus.status === "complete")
1216
+ const progressReadyToRestart =
1217
+ legacySession &&
1218
+ otaProgress?.currentUpdate === "bes" &&
1219
+ otaProgress.stage === "install" &&
1220
+ otaProgress.status === "FINISHED"
1221
+
1222
+ // progress-legacy treated a BES disconnect during active installation as the
1223
+ // expected power cycle even when its terminal FINISHED event was lost. Keep
1224
+ // that fallback only for the sticky pre-37 profile selected when the flow began.
1225
+ const legacyInstallInProgress =
1226
+ legacySession &&
1227
+ ((otaStatus?.stepType === "bes" && otaStatus.phase === "install" && otaStatus.status === "in_progress") ||
1228
+ (otaProgress?.currentUpdate === "bes" &&
1229
+ otaProgress.stage === "install" &&
1230
+ (otaProgress.status === "STARTED" || otaProgress.status === "PROGRESS")))
1231
+ return statusReadyToRestart || progressReadyToRestart || legacyInstallInProgress
1232
+ }
1233
+
1234
+ /** Complete only after a protocol-authorized BES restart traverses disconnect -> reconnect. */
1235
+ private completeBesRestartRecovery(): void {
1236
+ if (!this.besRestartDisconnectObserved || this.besRestartRecovery !== "awaiting") return
1237
+ console.log("[OTA_PROGRESS] BES restart reconnected — completing by reboot edge")
1238
+ this.clearPerStepTimers()
1239
+ this.setErrorMsg("")
1240
+ this.besRestartRecovery = "complete"
1241
+ this.emitInternalChange()
1242
+ }
1243
+
863
1244
  /**
864
1245
  * Legacy MTK install stall simulation (WP 8C-e), ported verbatim: the MTK system
865
1246
  * install typically stalls around 49-50%. Every real event restarts stall detection;
@@ -935,17 +1316,54 @@ class OtaInstallCoordinator {
935
1316
  sawReconnectEdge: this.sawReconnectEdge,
936
1317
  legacyApkSettleHold: this.legacyApkSettleHold,
937
1318
  apkCompletedViaBuildIncrease: this.apkCompletedViaBuildIncrease,
1319
+ besRestartRecovery: this.besRestartRecovery,
1320
+ versionChangeConverged: this.versionChangeConverged,
1321
+ versionChangeSession: this.versionChangeSession,
938
1322
  })
939
1323
  }
940
1324
 
941
1325
  /**
942
- * "Glasses are talking to us" clears the no-ack retry watchdog only.
1326
+ * During an APK install the package installer kills and restarts the glasses
1327
+ * process, and the new process's completion push can lose its startup race
1328
+ * against the UART transport (incident rep_01KY31HEMTSBSMK8DVMNXJ5XGG: the
1329
+ * phone sat on a stale "install in_progress 0%" until the stall watchdog
1330
+ * failed a successful update). The persisted session on the glasses always
1331
+ * knows the truth, so poll ota_query_status on the keepalive tick while an
1332
+ * apk step sits in its install phase — the reply either confirms progress or
1333
+ * delivers the missed completion. Gated to the apk install phase so a stray
1334
+ * idle reply can't disturb an active download; legacy (< 37) builds ignore
1335
+ * the query, which is harmless.
1336
+ *
1337
+ * The native query pends up to 15s and rejects a concurrent call with
1338
+ * request_in_flight — longer than the 10s tick — so exactly one poll is kept
1339
+ * in flight (the glasses being slow to answer IS the silent-restart window
1340
+ * this poll exists for) and rejections are swallowed: the next tick retries.
1341
+ */
1342
+ private maybePollApkInstallStatus(): void {
1343
+ if (this.isInVersionChangeDetour()) return
1344
+ if (this.apkInstallPollInFlight) return
1345
+ const s = useGlassesStore.getState().otaStatus
1346
+ if (s?.stepType === "apk" && s.phase === "install" && s.status === "in_progress") {
1347
+ this.apkInstallPollInFlight = true
1348
+ void BluetoothSdk.sendOtaQueryStatus()
1349
+ .catch(() => {})
1350
+ .finally(() => {
1351
+ this.apkInstallPollInFlight = false
1352
+ })
1353
+ }
1354
+ }
1355
+
1356
+ /**
1357
+ * "Glasses are talking to us" — suppresses retry if the native request later rejects.
943
1358
  * Important: this does NOT clear the stuck-at-zero watchdog; that one fires
944
1359
  * on real progress > 0% (see {@link onFirstNonZeroProgress}).
945
1360
  */
946
1361
  private onFirstActivity(): void {
947
1362
  if (this.hasFirstActivity) return
948
1363
  this.hasFirstActivity = true
1364
+ if (this.otaStartOwnership?.outcome === "pending") {
1365
+ this.otaStartOwnership.outcome = "acknowledged"
1366
+ }
949
1367
  this.clearRetryTimeout()
950
1368
  }
951
1369
 
@@ -968,7 +1386,8 @@ class OtaInstallCoordinator {
968
1386
  // Legacy-shape check happens once, when the session cap is armed at the first
969
1387
  // ota_start send (WP 8C-b): old builds get the padded legacy cap for the whole
970
1388
  // session; a session that starts unified keeps the unified cap.
971
- const globalTimeoutMs = this.isLegacySessionShapeNow() ? LEGACY_GLOBAL_OTA_TIMEOUT_MS : GLOBAL_OTA_TIMEOUT_MS
1389
+ const globalTimeoutMs =
1390
+ this.versionChangeSession || this.isLegacySessionShapeNow() ? LEGACY_GLOBAL_OTA_TIMEOUT_MS : GLOBAL_OTA_TIMEOUT_MS
972
1391
  this.globalTimeout = setTimeout(() => {
973
1392
  this.globalTimeout = null
974
1393
  this.globalTimeoutStarted = false
@@ -984,37 +1403,14 @@ class OtaInstallCoordinator {
984
1403
  }, globalTimeoutMs)
985
1404
  }
986
1405
 
987
- private armAckAndStuckWatchdogsOnly(): void {
988
- this.clearRetryTimeout()
1406
+ private armStuckWatchdogOnly(): void {
989
1407
  this.clearStuckTimeout()
990
1408
 
991
1409
  // Durations resolved at arm time (WP 8C-b): legacy-shaped sessions get the
992
1410
  // padded values progress-legacy.tsx used for old (< 37) builds.
993
1411
  const legacySession = this.isLegacySessionShapeNow()
994
- const retryIntervalMs = legacySession ? LEGACY_RETRY_INTERVAL_MS : RETRY_INTERVAL_MS
995
1412
  const stuckTimeoutMs = legacySession ? LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS : DOWNLOAD_STUCK_TIMEOUT_MS
996
1413
 
997
- this.retryTimeout = setTimeout(() => {
998
- this.retryTimeout = null
999
- if (this.hasReceivedAck || this.hasFirstActivity) return
1000
- if (this.computeDisplayStateNow() !== "starting") return
1001
- if (this.retryCount < MAX_RETRIES - 1) {
1002
- this.retryCount += 1
1003
- this.hasReceivedAck = false
1004
- console.log(
1005
- `[OTA_PROGRESS] watchdog: no ack in ${retryIntervalMs}ms, retrying ota_start (attempt ${this.retryCount})`,
1006
- )
1007
- void this.sendOtaStartWithWatchdogs(true)
1008
- .then(() => {
1009
- this.armAckAndStuckWatchdogsOnly()
1010
- })
1011
- .catch(() => {})
1012
- } else {
1013
- console.log(`[OTA_PROGRESS] watchdog: ota_start ack never received after ${MAX_RETRIES} attempts, failing`)
1014
- this.setErrorMsg(OtaProgressMessages.noAckResponse)
1015
- }
1016
- }, retryIntervalMs)
1017
-
1018
1414
  this.stuckTimeout = setTimeout(() => {
1019
1415
  this.stuckTimeout = null
1020
1416
  const d = this.computeDisplayStateNow()
@@ -1030,26 +1426,130 @@ class OtaInstallCoordinator {
1030
1426
  }, stuckTimeoutMs)
1031
1427
  }
1032
1428
 
1033
- private async sendOtaStartWithWatchdogs(retryAlreadyCounted = false): Promise<void> {
1429
+ /** Arm the 0%-stuck watchdog only when this logical session has none. */
1430
+ private maybeArmStuckWatchdog(): void {
1431
+ if (this.stuckTimeout || this.hasFirstNonZeroProgress) return
1432
+ this.armStuckWatchdogOnly()
1433
+ }
1434
+
1435
+ private sendOtaStartWithWatchdogs(): Promise<void> {
1436
+ // INVARIANT BACKSTOP — not normal control flow. Every entry point that can drive the
1437
+ // glasses (connect-edge, query fallback, retry, mount) carries its own detour gate with the
1438
+ // right behavior for that site; this final check only exists so that a FUTURE entry point
1439
+ // that forgets its gate degrades to a refused send + loud log instead of a corrupted
1440
+ // transaction (an ota_start mid-detour starts a parallel install on the factory build and
1441
+ // can trigger a second handoff). If this ever fires, a missed site gate exists — fix it.
1442
+ if (this.isInVersionChangeDetour()) {
1443
+ console.error(
1444
+ "[OTA_PROGRESS] BACKSTOP: refused ota_start during a latched version-change detour — a caller is missing its detour gate",
1445
+ )
1446
+ return Promise.resolve()
1447
+ }
1448
+ const existingOwnership = this.otaStartOwnership
1449
+ if (existingOwnership?.outcome === "pending") {
1450
+ this.maybeStartGlobalTimeout()
1451
+ if (this.selectedTransport === "wifi" || this.hotspotManifestUrl) this.maybeArmStuckWatchdog()
1452
+ console.log("[OTA_PROGRESS] ota_start already pending — adopting existing native request")
1453
+ return existingOwnership.promise
1454
+ }
1455
+ if (existingOwnership?.outcome === "acknowledged") {
1456
+ this.maybeStartGlobalTimeout()
1457
+ this.maybeArmStuckWatchdog()
1458
+ console.log("[OTA_PROGRESS] ota_start already acknowledged — refusing duplicate start")
1459
+ return Promise.resolve()
1460
+ }
1461
+ if (existingOwnership?.outcome === "rejected" && !this.prepareFreshOtaStartAttempt()) {
1462
+ return Promise.resolve()
1463
+ }
1034
1464
  this.maybeStartGlobalTimeout()
1035
- this.hasReceivedAck = false
1036
- this.armAckAndStuckWatchdogsOnly()
1465
+ if (this.selectedTransport === "wifi" || this.hotspotManifestUrl) this.maybeArmStuckWatchdog()
1466
+
1467
+ const ownership: OtaStartOwnership = {
1468
+ outcome: "pending",
1469
+ promise: Promise.resolve(),
1470
+ }
1471
+ ownership.promise = this.performOtaStart(ownership)
1472
+ this.otaStartOwnership = ownership
1473
+ return ownership.promise
1474
+ }
1475
+
1476
+ private async performOtaStart(ownership: OtaStartOwnership): Promise<void> {
1477
+ let nativeStartAttempted = false
1037
1478
  try {
1038
1479
  const state = useGlassesStore.getState()
1039
- const otaVersionUrl = getAsgOtaVersionUrl(state.otaVersionUrl, state.buildNumber)
1040
- console.log(`[OTA_PROGRESS] sending ota_start with manifest URL: ${otaVersionUrl}`)
1480
+ let otaVersionUrl = resolveOtaManifestUrl(state.otaVersionUrl, state.buildNumber)
1481
+ if (this.selectedTransport === "hotspot") {
1482
+ if (!this.hotspotManifestUrl) {
1483
+ if (!this.preparedCheckResult) {
1484
+ throw new Error("No selected OTA check is available for hotspot staging")
1485
+ }
1486
+ this.hotspotManifestUrl = await hotspotOtaTransport.prepare(this.preparedCheckResult, (progress) => {
1487
+ this.hotspotPhase = progress.phase
1488
+ this.hotspotArtifactPercent = progress.artifact?.artifactPercent ?? null
1489
+ this.emitInternalChange()
1490
+ })
1491
+ }
1492
+ otaVersionUrl = this.hotspotManifestUrl
1493
+ this.maybeArmStuckWatchdog()
1494
+ }
1495
+ if (!otaVersionUrl) {
1496
+ throw new Error("OTA is disabled because this build has no immutable manifest pin")
1497
+ }
1498
+ console.log(`[OTA_PROGRESS] sending ota_start with ${this.selectedTransport} manifest URL: ${otaVersionUrl}`)
1499
+ nativeStartAttempted = true
1041
1500
  await BluetoothSdk.startOtaUpdate(otaVersionUrl)
1501
+ if (this.otaStartOwnership !== ownership) return
1502
+ // The public SDK promise resolves only from ota_start_ack. Treat that
1503
+ // resolution as the acknowledgement even if the parallel event dispatch
1504
+ // reaches the coordinator later (or is dropped by a remount).
1505
+ this.handleAck()
1042
1506
  } catch (err) {
1043
1507
  console.warn("[OTA_PROGRESS] sendOtaStart threw", err)
1044
- this.clearRetryTimeout()
1508
+
1509
+ if (this.otaStartOwnership !== ownership) return
1510
+
1511
+ if (this.selectedTransport === "hotspot" && !nativeStartAttempted) {
1512
+ ownership.outcome = "rejected"
1513
+ this.clearStuckTimeout()
1514
+ await this.teardownHotspotTransport()
1515
+ if (this.attached) this.setErrorMsg(hotspotPreflightErrorMessage(err))
1516
+ return
1517
+ }
1518
+
1519
+ // A received ota_status/legacy progress event, or the exact legacy APK
1520
+ // build-number proof, proves that this logical start was accepted. Its
1521
+ // application-level ack may be lost while the update continues; never
1522
+ // create another transaction or fail a live, restarting, or completed
1523
+ // session because that stale promise timed out.
1524
+ if (this.hasFirstActivity || this.apkCompletedViaBuildIncrease) {
1525
+ ownership.outcome = "acknowledged"
1526
+ console.log("[OTA_PROGRESS] ota_start rejection arrived after acceptance proof — ignoring stale rejection")
1527
+ return
1528
+ }
1529
+
1530
+ ownership.outcome = "rejected"
1531
+ if (!this.attached) return
1045
1532
  this.clearStuckTimeout()
1533
+ if (!isGlassesConnected(useGlassesStore.getState().connection)) {
1534
+ console.log(
1535
+ "[OTA_PROGRESS] ota_start request ended while disconnected; deferring retry to reconnect arbitration",
1536
+ )
1537
+ return
1538
+ }
1046
1539
  if (this.retryCount < MAX_RETRIES - 1) {
1047
- if (!retryAlreadyCounted) {
1048
- this.retryCount += 1
1049
- }
1540
+ this.retryCount += 1
1050
1541
  const retryIntervalMs = this.isLegacySessionShapeNow() ? LEGACY_RETRY_INTERVAL_MS : RETRY_INTERVAL_MS
1542
+ console.log(
1543
+ `[OTA_PROGRESS] ota_start request ended without ack or activity; retrying after ${retryIntervalMs}ms (attempt ${
1544
+ this.retryCount + 1
1545
+ }/${MAX_RETRIES})`,
1546
+ )
1051
1547
  this.retryTimeout = setTimeout(() => {
1052
1548
  this.retryTimeout = null
1549
+ if (!isGlassesConnected(useGlassesStore.getState().connection)) {
1550
+ console.log("[OTA_PROGRESS] ota_start retry paused while disconnected; reconnect arbitration will resume")
1551
+ return
1552
+ }
1053
1553
  void this.sendOtaStartWithWatchdogs()
1054
1554
  }, retryIntervalMs)
1055
1555
  } else {
@@ -1059,18 +1559,28 @@ class OtaInstallCoordinator {
1059
1559
  }
1060
1560
  }
1061
1561
 
1562
+ private async teardownHotspotTransport(): Promise<void> {
1563
+ if (this.selectedTransport !== "hotspot" && !this.hotspotManifestUrl) return
1564
+ await hotspotOtaTransport.teardown()
1565
+ this.hotspotManifestUrl = null
1566
+ this.hotspotPhase = "idle"
1567
+ this.hotspotArtifactPercent = null
1568
+ this.emitInternalChange()
1569
+ }
1570
+
1062
1571
  /**
1063
1572
  * After sending ota_query_status, wait QUERY_REPLY_TIMEOUT_MS for the glasses
1064
- * to reply with a useful ota_status. If nothing arrives (e.g. the glasses'
1065
- * OTA session was wiped between mount and reconnect), or the glasses reply
1066
- * with an explicit "idle" status (no session), fall back to ota_start so the
1067
- * user doesn't sit on a spinner forever.
1573
+ * to reply with a useful ota_status. A unified session may start again only
1574
+ * after an explicit "idle" reply, or after a locally rejected ota_start has
1575
+ * produced no activity and no authoritative reply across reconnect. Legacy
1576
+ * builds ignore ota_query_status, so their established timeout fallback
1577
+ * remains the compatibility path.
1068
1578
  *
1069
1579
  * Cleared as soon as a non-idle otaStatus or any otaProgress lands in the
1070
1580
  * store (see the reaction block above). An idle reply intentionally does NOT
1071
1581
  * cancel the fallback, so reconnects against a wiped/lost session still recover.
1072
1582
  */
1073
- private armQueryReplyFallback(reason: "reconnect" | "initial-mount"): void {
1583
+ private armQueryReplyFallback(reason: "reconnect" | "initial-mount" | "retry"): void {
1074
1584
  this.clearQueryReplyTimeout()
1075
1585
  this.queryReplyTimeout = setTimeout(() => {
1076
1586
  this.queryReplyTimeout = null
@@ -1081,21 +1591,24 @@ class OtaInstallCoordinator {
1081
1591
  // whatever legacy-shaped status/progress sits in the store is a pre-query
1082
1592
  // leftover, not a reply — a NEW legacy event would have cleared this timer
1083
1593
  // through the reaction pass. Only a unified reply suppresses the fallback.
1084
- const legacySession = isLegacyShapedOtaSession(
1085
- state.otaStatus,
1086
- state.otaProgress,
1087
- this.initialBuildNumber || state.buildNumber,
1088
- )
1594
+ const legacySession = this.isLegacySessionShapeNow()
1089
1595
  if (!legacySession && hasRecoveringOtaReply(state.otaStatus, state.otaProgress)) return
1596
+ const explicitIdle = state.otaStatus?.status === "idle"
1597
+ const rejectedStart = this.otaStartOwnership?.outcome === "rejected"
1598
+ if (!legacySession && !explicitIdle && !rejectedStart) {
1599
+ console.log(
1600
+ `[OTA_PROGRESS] watchdog: ota_query_status got no authoritative idle reply in ${QUERY_REPLY_TIMEOUT_MS}ms (reason=${reason}); preserving the existing session`,
1601
+ )
1602
+ return
1603
+ }
1090
1604
  // Don't fire if we've left the active phase (e.g. user backed out, error overlay).
1091
1605
  if (isTerminalForWatchdog(this.computeDisplayStateNow())) return
1092
- console.log(
1093
- `[OTA_PROGRESS] watchdog: ota_query_status got no useful reply in ${QUERY_REPLY_TIMEOUT_MS}ms (reason=${reason}), falling back to ota_start`,
1094
- )
1606
+ let fallbackCause = "legacy query timeout"
1607
+ if (explicitIdle) fallbackCause = "idle ota_status"
1608
+ else if (rejectedStart) fallbackCause = "rejected ota_start with no recovery reply"
1609
+ console.log(`[OTA_PROGRESS] watchdog: ${fallbackCause} (reason=${reason}), falling back to ota_start`)
1095
1610
  this.retryCount = 0
1096
- this.hasFirstActivity = false
1097
- this.hasFirstNonZeroProgress = false
1098
- this.hasReceivedAck = false
1611
+ if (this.otaStartOwnership?.outcome !== "pending" && !this.prepareFreshOtaStartAttempt()) return
1099
1612
  void this.sendOtaStartWithWatchdogs()
1100
1613
  }, QUERY_REPLY_TIMEOUT_MS)
1101
1614
  }
@@ -1131,13 +1644,6 @@ class OtaInstallCoordinator {
1131
1644
  this.globalTimeoutStarted = false
1132
1645
  }
1133
1646
 
1134
- private clearPostApkDelay(): void {
1135
- if (this.postApkDelay) {
1136
- clearTimeout(this.postApkDelay)
1137
- this.postApkDelay = null
1138
- }
1139
- }
1140
-
1141
1647
  private clearPingInterval(): void {
1142
1648
  if (this.pingInterval) {
1143
1649
  clearInterval(this.pingInterval)
@@ -1188,7 +1694,6 @@ class OtaInstallCoordinator {
1188
1694
  this.clearRetryTimeout()
1189
1695
  this.clearStuckTimeout()
1190
1696
  this.clearProgressTimeout()
1191
- this.clearPostApkDelay()
1192
1697
  this.clearQueryReplyTimeout()
1193
1698
  }
1194
1699