@mentra/engine 0.1.0-dev.0 → 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) hide show
  1. package/README.md +42 -1
  2. package/build/engine.d.ts +11 -3
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -2
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/dev.d.ts +3 -2
  7. package/build/facades/dev.d.ts.map +1 -1
  8. package/build/facades/dev.js +18 -12
  9. package/build/facades/dev.js.map +1 -1
  10. package/build/facades/gallery.js +1 -1
  11. package/build/facades/gallery.js.map +1 -1
  12. package/build/facades/glasses.d.ts +1 -0
  13. package/build/facades/glasses.d.ts.map +1 -1
  14. package/build/facades/glasses.js +19 -4
  15. package/build/facades/glasses.js.map +1 -1
  16. package/build/facades/glassesSettings.d.ts.map +1 -1
  17. package/build/facades/glassesSettings.js +1 -0
  18. package/build/facades/glassesSettings.js.map +1 -1
  19. package/build/facades/miniapps.d.ts +4 -0
  20. package/build/facades/miniapps.d.ts.map +1 -1
  21. package/build/facades/miniapps.js +5 -0
  22. package/build/facades/miniapps.js.map +1 -1
  23. package/build/facades/ota.d.ts +11 -2
  24. package/build/facades/ota.d.ts.map +1 -1
  25. package/build/facades/ota.js +19 -4
  26. package/build/facades/ota.js.map +1 -1
  27. package/build/facades/pairing.d.ts +4 -0
  28. package/build/facades/pairing.d.ts.map +1 -1
  29. package/build/facades/pairing.js +10 -8
  30. package/build/facades/pairing.js.map +1 -1
  31. package/build/facades/permissions.d.ts +10 -0
  32. package/build/facades/permissions.d.ts.map +1 -1
  33. package/build/facades/permissions.js +10 -0
  34. package/build/facades/permissions.js.map +1 -1
  35. package/build/facades/phoneNotifications.d.ts +0 -4
  36. package/build/facades/phoneNotifications.d.ts.map +1 -1
  37. package/build/facades/phoneNotifications.js +4 -14
  38. package/build/facades/phoneNotifications.js.map +1 -1
  39. package/build/index.d.ts +6 -3
  40. package/build/index.d.ts.map +1 -1
  41. package/build/index.js +4 -2
  42. package/build/index.js.map +1 -1
  43. package/build/internal.d.ts +3 -2
  44. package/build/internal.d.ts.map +1 -1
  45. package/build/internal.js +2 -1
  46. package/build/internal.js.map +1 -1
  47. package/build/react/MentraLiveOtaFlow.d.ts +38 -0
  48. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  49. package/build/react/MentraLiveOtaFlow.js +528 -0
  50. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  51. package/build/react/index.d.ts +2 -0
  52. package/build/react/index.d.ts.map +1 -0
  53. package/build/react/index.js +2 -0
  54. package/build/react/index.js.map +1 -0
  55. package/build/react/useEngineSnapshot.d.ts +2 -0
  56. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  57. package/build/react/useEngineSnapshot.js +14 -0
  58. package/build/react/useEngineSnapshot.js.map +1 -0
  59. package/build/runtime/bootstrap.d.ts +24 -1
  60. package/build/runtime/bootstrap.d.ts.map +1 -1
  61. package/build/runtime/bootstrap.js +17 -0
  62. package/build/runtime/bootstrap.js.map +1 -1
  63. package/build/runtime/scanQrSeam.d.ts +7 -0
  64. package/build/runtime/scanQrSeam.d.ts.map +1 -0
  65. package/build/runtime/scanQrSeam.js +16 -0
  66. package/build/runtime/scanQrSeam.js.map +1 -0
  67. package/build/services/AppRegistry.d.ts +33 -57
  68. package/build/services/AppRegistry.d.ts.map +1 -1
  69. package/build/services/AppRegistry.js +193 -144
  70. package/build/services/AppRegistry.js.map +1 -1
  71. package/build/services/AudioCloudUplink.d.ts +12 -0
  72. package/build/services/AudioCloudUplink.d.ts.map +1 -1
  73. package/build/services/AudioCloudUplink.js +22 -1
  74. package/build/services/AudioCloudUplink.js.map +1 -1
  75. package/build/services/AudioPlaybackService.d.ts +41 -2
  76. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  77. package/build/services/AudioPlaybackService.js +250 -33
  78. package/build/services/AudioPlaybackService.js.map +1 -1
  79. package/build/services/BlobStore.d.ts +2 -1
  80. package/build/services/BlobStore.d.ts.map +1 -1
  81. package/build/services/BlobStore.js +17 -9
  82. package/build/services/BlobStore.js.map +1 -1
  83. package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
  84. package/build/services/CloudAudioSubscriptionSync.js +13 -1
  85. package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
  86. package/build/services/CloudClientService.d.ts +15 -0
  87. package/build/services/CloudClientService.d.ts.map +1 -1
  88. package/build/services/CloudClientService.js +93 -1
  89. package/build/services/CloudClientService.js.map +1 -1
  90. package/build/services/DeviceEventRouter.js +5 -5
  91. package/build/services/DeviceEventRouter.js.map +1 -1
  92. package/build/services/ForegroundLocationPermission.d.ts +13 -0
  93. package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
  94. package/build/services/ForegroundLocationPermission.js +12 -0
  95. package/build/services/ForegroundLocationPermission.js.map +1 -0
  96. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  97. package/build/services/GlassesSettingsSync.js +9 -3
  98. package/build/services/GlassesSettingsSync.js.map +1 -1
  99. package/build/services/GlassesStatusProjection.d.ts +2 -1
  100. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  101. package/build/services/GlassesStatusProjection.js +15 -8
  102. package/build/services/GlassesStatusProjection.js.map +1 -1
  103. package/build/services/HotspotOtaTransport.d.ts +29 -0
  104. package/build/services/HotspotOtaTransport.d.ts.map +1 -0
  105. package/build/services/HotspotOtaTransport.js +146 -0
  106. package/build/services/HotspotOtaTransport.js.map +1 -0
  107. package/build/services/HotspotShutdown.d.ts +17 -0
  108. package/build/services/HotspotShutdown.d.ts.map +1 -0
  109. package/build/services/HotspotShutdown.js +29 -0
  110. package/build/services/HotspotShutdown.js.map +1 -0
  111. package/build/services/LocalDisplayManager.d.ts +15 -0
  112. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  113. package/build/services/LocalDisplayManager.js +106 -20
  114. package/build/services/LocalDisplayManager.js.map +1 -1
  115. package/build/services/LocalMiniappRuntime.d.ts +74 -2
  116. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  117. package/build/services/LocalMiniappRuntime.js +785 -291
  118. package/build/services/LocalMiniappRuntime.js.map +1 -1
  119. package/build/services/LocalMiniappStorage.d.ts +32 -0
  120. package/build/services/LocalMiniappStorage.d.ts.map +1 -0
  121. package/build/services/LocalMiniappStorage.js +66 -0
  122. package/build/services/LocalMiniappStorage.js.map +1 -0
  123. package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
  124. package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
  125. package/build/services/LocalMiniappUserIdentity.js +43 -0
  126. package/build/services/LocalMiniappUserIdentity.js.map +1 -0
  127. package/build/services/LocalSttFallbackCoordinator.d.ts +9 -6
  128. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  129. package/build/services/LocalSttFallbackCoordinator.js +18 -12
  130. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  131. package/build/services/MicStateCoordinator.d.ts +42 -1
  132. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  133. package/build/services/MicStateCoordinator.js +141 -3
  134. package/build/services/MicStateCoordinator.js.map +1 -1
  135. package/build/services/MiniappLauncher.d.ts.map +1 -1
  136. package/build/services/MiniappLauncher.js +23 -3
  137. package/build/services/MiniappLauncher.js.map +1 -1
  138. package/build/services/MiniappLiveness.d.ts +12 -0
  139. package/build/services/MiniappLiveness.d.ts.map +1 -0
  140. package/build/services/MiniappLiveness.js +16 -0
  141. package/build/services/MiniappLiveness.js.map +1 -0
  142. package/build/services/NativeHttpResponse.d.ts +3 -0
  143. package/build/services/NativeHttpResponse.d.ts.map +1 -0
  144. package/build/services/NativeHttpResponse.js +6 -0
  145. package/build/services/NativeHttpResponse.js.map +1 -0
  146. package/build/services/OtaArtifactDownloader.d.ts +65 -0
  147. package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
  148. package/build/services/OtaArtifactDownloader.js +226 -0
  149. package/build/services/OtaArtifactDownloader.js.map +1 -0
  150. package/build/services/OtaAutoChain.d.ts +34 -0
  151. package/build/services/OtaAutoChain.d.ts.map +1 -0
  152. package/build/services/OtaAutoChain.js +83 -0
  153. package/build/services/OtaAutoChain.js.map +1 -0
  154. package/build/services/OtaErrorMapping.d.ts +18 -0
  155. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  156. package/build/services/OtaErrorMapping.js +63 -0
  157. package/build/services/OtaErrorMapping.js.map +1 -0
  158. package/build/services/OtaInstallCoordinator.d.ts +94 -14
  159. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  160. package/build/services/OtaInstallCoordinator.js +574 -130
  161. package/build/services/OtaInstallCoordinator.js.map +1 -1
  162. package/build/services/OtaService.d.ts.map +1 -1
  163. package/build/services/OtaService.js +8 -1
  164. package/build/services/OtaService.js.map +1 -1
  165. package/build/services/OtaUpdateCheckService.d.ts +38 -3
  166. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  167. package/build/services/OtaUpdateCheckService.js +142 -22
  168. package/build/services/OtaUpdateCheckService.js.map +1 -1
  169. package/build/services/PhoneCalendarHelpers.d.ts +32 -0
  170. package/build/services/PhoneCalendarHelpers.d.ts.map +1 -0
  171. package/build/services/PhoneCalendarHelpers.js +157 -0
  172. package/build/services/PhoneCalendarHelpers.js.map +1 -0
  173. package/build/services/PhoneCalendarService.d.ts +5 -0
  174. package/build/services/PhoneCalendarService.d.ts.map +1 -0
  175. package/build/services/PhoneCalendarService.js +45 -0
  176. package/build/services/PhoneCalendarService.js.map +1 -0
  177. package/build/services/PhoneCameraFovCoordinator.d.ts +30 -0
  178. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -0
  179. package/build/services/PhoneCameraFovCoordinator.js +194 -0
  180. package/build/services/PhoneCameraFovCoordinator.js.map +1 -0
  181. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  182. package/build/services/PhoneNotificationsSync.js +12 -10
  183. package/build/services/PhoneNotificationsSync.js.map +1 -1
  184. package/build/services/PhonePhotoCoordinator.d.ts +26 -2
  185. package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
  186. package/build/services/PhonePhotoCoordinator.js +79 -9
  187. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  188. package/build/services/PhoneStreamCoordinator.d.ts +8 -1
  189. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  190. package/build/services/PhoneStreamCoordinator.js +125 -13
  191. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  192. package/build/services/PhoneVideoCoordinator.d.ts +2 -0
  193. package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
  194. package/build/services/PhoneVideoCoordinator.js +10 -0
  195. package/build/services/PhoneVideoCoordinator.js.map +1 -1
  196. package/build/services/SceneRenderer.d.ts.map +1 -1
  197. package/build/services/SceneRenderer.js +15 -0
  198. package/build/services/SceneRenderer.js.map +1 -1
  199. package/build/services/SentenceTtsPipeline.d.ts +31 -0
  200. package/build/services/SentenceTtsPipeline.d.ts.map +1 -0
  201. package/build/services/SentenceTtsPipeline.js +67 -0
  202. package/build/services/SentenceTtsPipeline.js.map +1 -0
  203. package/build/services/SupportProfileSync.d.ts +22 -0
  204. package/build/services/SupportProfileSync.d.ts.map +1 -0
  205. package/build/services/SupportProfileSync.js +213 -0
  206. package/build/services/SupportProfileSync.js.map +1 -0
  207. package/build/services/TranscriptionRouting.d.ts +12 -0
  208. package/build/services/TranscriptionRouting.d.ts.map +1 -0
  209. package/build/services/TranscriptionRouting.js +24 -0
  210. package/build/services/TranscriptionRouting.js.map +1 -0
  211. package/build/services/TtsTextSanitizer.d.ts +5 -0
  212. package/build/services/TtsTextSanitizer.d.ts.map +1 -0
  213. package/build/services/TtsTextSanitizer.js +61 -0
  214. package/build/services/TtsTextSanitizer.js.map +1 -0
  215. package/build/services/WifiAdbRequest.d.ts +17 -0
  216. package/build/services/WifiAdbRequest.d.ts.map +1 -0
  217. package/build/services/WifiAdbRequest.js +35 -0
  218. package/build/services/WifiAdbRequest.js.map +1 -0
  219. package/build/services/asg/asgCameraApi.d.ts +43 -0
  220. package/build/services/asg/asgCameraApi.d.ts.map +1 -1
  221. package/build/services/asg/asgCameraApi.js +509 -26
  222. package/build/services/asg/asgCameraApi.js.map +1 -1
  223. package/build/services/asg/cameraRollExportCoordinator.d.ts +60 -0
  224. package/build/services/asg/cameraRollExportCoordinator.d.ts.map +1 -0
  225. package/build/services/asg/cameraRollExportCoordinator.js +618 -0
  226. package/build/services/asg/cameraRollExportCoordinator.js.map +1 -0
  227. package/build/services/asg/cameraRollExportLedger.d.ts +44 -0
  228. package/build/services/asg/cameraRollExportLedger.d.ts.map +1 -0
  229. package/build/services/asg/cameraRollExportLedger.js +78 -0
  230. package/build/services/asg/cameraRollExportLedger.js.map +1 -0
  231. package/build/services/asg/galleryNotices.d.ts +1 -1
  232. package/build/services/asg/galleryNotices.d.ts.map +1 -1
  233. package/build/services/asg/galleryNotices.js.map +1 -1
  234. package/build/services/asg/galleryPathMigration.d.ts +3 -0
  235. package/build/services/asg/galleryPathMigration.d.ts.map +1 -0
  236. package/build/services/asg/galleryPathMigration.js +26 -0
  237. package/build/services/asg/galleryPathMigration.js.map +1 -0
  238. package/build/services/asg/gallerySettingsService.d.ts +1 -0
  239. package/build/services/asg/gallerySettingsService.d.ts.map +1 -1
  240. package/build/services/asg/gallerySettingsService.js +2 -0
  241. package/build/services/asg/gallerySettingsService.js.map +1 -1
  242. package/build/services/asg/gallerySyncService.d.ts.map +1 -1
  243. package/build/services/asg/gallerySyncService.js +162 -56
  244. package/build/services/asg/gallerySyncService.js.map +1 -1
  245. package/build/services/asg/galleryTransferLedger.d.ts +58 -0
  246. package/build/services/asg/galleryTransferLedger.d.ts.map +1 -0
  247. package/build/services/asg/galleryTransferLedger.js +225 -0
  248. package/build/services/asg/galleryTransferLedger.js.map +1 -0
  249. package/build/services/asg/localNetworkTransport.d.ts +15 -0
  250. package/build/services/asg/localNetworkTransport.d.ts.map +1 -0
  251. package/build/services/asg/localNetworkTransport.js +144 -0
  252. package/build/services/asg/localNetworkTransport.js.map +1 -0
  253. package/build/services/asg/localStorageService.d.ts +18 -0
  254. package/build/services/asg/localStorageService.d.ts.map +1 -1
  255. package/build/services/asg/localStorageService.js +168 -19
  256. package/build/services/asg/localStorageService.js.map +1 -1
  257. package/build/services/asg/mediaProcessingQueue.d.ts +10 -0
  258. package/build/services/asg/mediaProcessingQueue.d.ts.map +1 -1
  259. package/build/services/asg/mediaProcessingQueue.js +193 -62
  260. package/build/services/asg/mediaProcessingQueue.js.map +1 -1
  261. package/build/services/audioPlaybackAssets.d.ts +8 -0
  262. package/build/services/audioPlaybackAssets.d.ts.map +1 -0
  263. package/build/services/audioPlaybackAssets.js +7 -0
  264. package/build/services/audioPlaybackAssets.js.map +1 -0
  265. package/build/services/gallerySyncClock.js +1 -1
  266. package/build/services/gallerySyncClock.js.map +1 -1
  267. package/build/services/glassesClockSync.js +2 -2
  268. package/build/services/glassesClockSync.js.map +1 -1
  269. package/build/services/manifestActions.d.ts +7 -0
  270. package/build/services/manifestActions.d.ts.map +1 -0
  271. package/build/services/manifestActions.js +25 -0
  272. package/build/services/manifestActions.js.map +1 -0
  273. package/build/services/otaDisplayState.d.ts +22 -11
  274. package/build/services/otaDisplayState.d.ts.map +1 -1
  275. package/build/services/otaDisplayState.js +36 -14
  276. package/build/services/otaDisplayState.js.map +1 -1
  277. package/build/services/otaInstallPolicy.d.ts +15 -3
  278. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  279. package/build/services/otaInstallPolicy.js +24 -3
  280. package/build/services/otaInstallPolicy.js.map +1 -1
  281. package/build/services/otaManifestUrl.d.ts +2 -0
  282. package/build/services/otaManifestUrl.d.ts.map +1 -0
  283. package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
  284. package/build/services/otaManifestUrl.js.map +1 -0
  285. package/build/services/slimStreamStatus.d.ts +11 -0
  286. package/build/services/slimStreamStatus.d.ts.map +1 -1
  287. package/build/services/slimStreamStatus.js +14 -4
  288. package/build/services/slimStreamStatus.js.map +1 -1
  289. package/build/stores/apps.d.ts.map +1 -1
  290. package/build/stores/apps.js +11 -0
  291. package/build/stores/apps.js.map +1 -1
  292. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  293. package/build/stores/bluetoothSettingKeys.js +2 -0
  294. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  295. package/build/stores/gallerySync.d.ts +2 -0
  296. package/build/stores/gallerySync.d.ts.map +1 -1
  297. package/build/stores/gallerySync.js +2 -0
  298. package/build/stores/gallerySync.js.map +1 -1
  299. package/build/stores/glasses.d.ts.map +1 -1
  300. package/build/stores/glasses.js +3 -0
  301. package/build/stores/glasses.js.map +1 -1
  302. package/build/stores/settings.d.ts +17 -0
  303. package/build/stores/settings.d.ts.map +1 -1
  304. package/build/stores/settings.js +157 -17
  305. package/build/stores/settings.js.map +1 -1
  306. package/build/types/applet.d.ts +2 -0
  307. package/build/types/applet.d.ts.map +1 -1
  308. package/build/types/applet.js.map +1 -1
  309. package/build/types/asg.d.ts +7 -0
  310. package/build/types/asg.d.ts.map +1 -1
  311. package/build/types/asg.js.map +1 -1
  312. package/build/types/enums.d.ts +1 -0
  313. package/build/types/enums.d.ts.map +1 -1
  314. package/build/types/enums.js +1 -0
  315. package/build/types/enums.js.map +1 -1
  316. package/build/types/hardware.d.ts +1 -0
  317. package/build/types/hardware.d.ts.map +1 -1
  318. package/build/types/hardware.js +14 -0
  319. package/build/types/hardware.js.map +1 -1
  320. package/build/utils/configuredDevHost.d.ts +6 -0
  321. package/build/utils/configuredDevHost.d.ts.map +1 -0
  322. package/build/utils/configuredDevHost.js +19 -0
  323. package/build/utils/configuredDevHost.js.map +1 -0
  324. package/build/utils/devMiniappLaunch.d.ts +19 -8
  325. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  326. package/build/utils/devMiniappLaunch.js +144 -21
  327. package/build/utils/devMiniappLaunch.js.map +1 -1
  328. package/build/utils/diagnosticContext.d.ts.map +1 -1
  329. package/build/utils/diagnosticContext.js +15 -17
  330. package/build/utils/diagnosticContext.js.map +1 -1
  331. package/build/utils/hardware/hardware.d.ts.map +1 -1
  332. package/build/utils/hardware/hardware.js +7 -3
  333. package/build/utils/hardware/hardware.js.map +1 -1
  334. package/build/utils/miniappDiagnostics.d.ts +64 -0
  335. package/build/utils/miniappDiagnostics.d.ts.map +1 -0
  336. package/build/utils/miniappDiagnostics.js +173 -0
  337. package/build/utils/miniappDiagnostics.js.map +1 -0
  338. package/build/utils/permissions/MediaLibraryPermissions.d.ts +11 -0
  339. package/build/utils/permissions/MediaLibraryPermissions.d.ts.map +1 -1
  340. package/build/utils/permissions/MediaLibraryPermissions.js +27 -4
  341. package/build/utils/permissions/MediaLibraryPermissions.js.map +1 -1
  342. package/build/utils/timers/timers.d.ts.map +1 -1
  343. package/build/utils/timers/timers.js +55 -18
  344. package/build/utils/timers/timers.js.map +1 -1
  345. package/package.json +25 -15
  346. package/src/assets/silence.wav +0 -0
  347. package/src/engine.ts +7 -2
  348. package/src/facades/dev.ts +17 -12
  349. package/src/facades/gallery.ts +1 -1
  350. package/src/facades/glasses.ts +17 -4
  351. package/src/facades/glassesSettings.ts +5 -0
  352. package/src/facades/miniapps.ts +7 -0
  353. package/src/facades/ota.ts +19 -4
  354. package/src/facades/pairing.ts +11 -7
  355. package/src/facades/permissions.ts +14 -0
  356. package/src/facades/phoneNotifications.ts +4 -13
  357. package/src/index.ts +26 -2
  358. package/src/internal.ts +3 -1
  359. package/src/react/MentraLiveOtaFlow.tsx +856 -0
  360. package/src/react/index.ts +7 -0
  361. package/src/react/useEngineSnapshot.ts +16 -0
  362. package/src/runtime/bootstrap.ts +33 -1
  363. package/src/runtime/scanQrSeam.ts +20 -0
  364. package/src/services/AppRegistry.ts +224 -149
  365. package/src/services/AudioCloudUplink.ts +23 -1
  366. package/src/services/AudioPlaybackService.ts +287 -38
  367. package/src/services/BlobStore.ts +22 -9
  368. package/src/services/CloudAudioSubscriptionSync.ts +13 -1
  369. package/src/services/CloudClientService.ts +96 -1
  370. package/src/services/DeviceEventRouter.ts +5 -5
  371. package/src/services/ForegroundLocationPermission.ts +24 -0
  372. package/src/services/GlassesSettingsSync.ts +9 -3
  373. package/src/services/GlassesStatusProjection.ts +16 -8
  374. package/src/services/HotspotOtaTransport.ts +184 -0
  375. package/src/services/HotspotShutdown.ts +41 -0
  376. package/src/services/LocalDisplayManager.ts +112 -21
  377. package/src/services/LocalMiniappRuntime.ts +967 -346
  378. package/src/services/LocalMiniappStorage.ts +82 -0
  379. package/src/services/LocalMiniappUserIdentity.ts +46 -0
  380. package/src/services/LocalSttFallbackCoordinator.ts +19 -12
  381. package/src/services/MicStateCoordinator.ts +169 -4
  382. package/src/services/MiniappLauncher.ts +30 -3
  383. package/src/services/MiniappLiveness.ts +24 -0
  384. package/src/services/NativeHttpResponse.ts +6 -0
  385. package/src/services/OtaArtifactDownloader.ts +312 -0
  386. package/src/services/OtaAutoChain.ts +106 -0
  387. package/src/services/OtaErrorMapping.ts +80 -0
  388. package/src/services/OtaInstallCoordinator.ts +645 -143
  389. package/src/services/OtaService.ts +11 -5
  390. package/src/services/OtaUpdateCheckService.ts +180 -20
  391. package/src/services/PhoneCalendarHelpers.ts +212 -0
  392. package/src/services/PhoneCalendarService.ts +65 -0
  393. package/src/services/PhoneCameraFovCoordinator.ts +210 -0
  394. package/src/services/PhoneNotificationsSync.ts +12 -10
  395. package/src/services/PhonePhotoCoordinator.ts +116 -18
  396. package/src/services/PhoneStreamCoordinator.ts +139 -25
  397. package/src/services/PhoneVideoCoordinator.ts +11 -0
  398. package/src/services/SceneRenderer.ts +16 -0
  399. package/src/services/SentenceTtsPipeline.ts +106 -0
  400. package/src/services/SupportProfileSync.ts +218 -0
  401. package/src/services/TranscriptionRouting.ts +37 -0
  402. package/src/services/TtsTextSanitizer.ts +66 -0
  403. package/src/services/WifiAdbRequest.ts +53 -0
  404. package/src/services/asg/asgCameraApi.ts +615 -33
  405. package/src/services/asg/cameraRollExportCoordinator.ts +627 -0
  406. package/src/services/asg/cameraRollExportLedger.ts +122 -0
  407. package/src/services/asg/galleryNotices.ts +1 -0
  408. package/src/services/asg/galleryPathMigration.ts +22 -0
  409. package/src/services/asg/gallerySettingsService.ts +3 -0
  410. package/src/services/asg/gallerySyncService.ts +184 -73
  411. package/src/services/asg/galleryTransferLedger.ts +312 -0
  412. package/src/services/asg/localNetworkTransport.ts +169 -0
  413. package/src/services/asg/localStorageService.ts +179 -24
  414. package/src/services/asg/mediaProcessingQueue.ts +209 -62
  415. package/src/services/audioPlaybackAssets.ts +8 -0
  416. package/src/services/gallerySyncClock.ts +1 -1
  417. package/src/services/glassesClockSync.ts +2 -2
  418. package/src/services/manifestActions.ts +30 -0
  419. package/src/services/otaDisplayState.ts +57 -16
  420. package/src/services/otaInstallPolicy.ts +29 -3
  421. package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
  422. package/src/services/slimStreamStatus.ts +16 -5
  423. package/src/stores/apps.ts +12 -0
  424. package/src/stores/bluetoothSettingKeys.ts +2 -0
  425. package/src/stores/gallerySync.ts +4 -0
  426. package/src/stores/glasses.ts +3 -0
  427. package/src/stores/settings.ts +179 -18
  428. package/src/types/applet.ts +2 -0
  429. package/src/types/asg.ts +7 -0
  430. package/src/types/enums.ts +2 -1
  431. package/src/types/hardware.ts +17 -0
  432. package/src/utils/configuredDevHost.ts +17 -0
  433. package/src/utils/devMiniappLaunch.ts +151 -23
  434. package/src/utils/diagnosticContext.ts +17 -17
  435. package/src/utils/hardware/hardware.ts +7 -9
  436. package/src/utils/miniappDiagnostics.ts +226 -0
  437. package/src/utils/permissions/MediaLibraryPermissions.ts +35 -4
  438. package/src/utils/timers/timers.ts +57 -21
  439. package/build/services/asgOtaVersionUrl.d.ts +0 -2
  440. package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
  441. package/build/services/asgOtaVersionUrl.js.map +0 -1
@@ -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,10 +11,7 @@
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
@@ -23,12 +20,33 @@
23
20
  import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
24
21
  import GlobalEventEmitter from "../utils/GlobalEventEmitter";
25
22
  import { isGlassesConnected, useGlassesStore } from "../stores/glasses";
26
- import { getAsgOtaVersionUrl } from "./asgOtaVersionUrl";
23
+ import { resolveOtaManifestUrl } from "./otaManifestUrl";
24
+ import { hotspotOtaTransport } from "./HotspotOtaTransport";
27
25
  import { deriveDisplayState } from "./otaDisplayState";
28
- import { BES_CONTINUE_LOCKOUT_MS, DOWNLOAD_STUCK_TIMEOUT_MS, GLOBAL_OTA_TIMEOUT_MS, LEGACY_APK_COMPLETION_SETTLE_MS, LEGACY_BES_CONTINUE_LOCKOUT_MS, LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS, LEGACY_GLOBAL_OTA_TIMEOUT_MS, LEGACY_MTK_INSTALL_TIMEOUT_MS, LEGACY_MTK_SIM_CAP_PERCENT, LEGACY_MTK_SIM_FLOOR_PERCENT, LEGACY_MTK_SIM_TICK_MS, LEGACY_MTK_STALL_DETECT_MS, LEGACY_MTK_STALL_ZONE_MAX_PERCENT, LEGACY_MTK_STALL_ZONE_MIN_PERCENT, LEGACY_PING_INTERVAL_MS, LEGACY_PROGRESS_TIMEOUT_MS, LEGACY_RETRY_INTERVAL_MS, MAX_RETRIES, MTK_INSTALL_TIMEOUT_MS, OtaProgressMessages, PING_INTERVAL_MS, POST_APK_OTA_START_DELAY_MS, PROGRESS_TIMEOUT_MS, QUERY_REPLY_TIMEOUT_MS, RETRY_INTERVAL_MS, isLegacyShapedOtaSession, isLegacyShapedOtaStatus, } from "./otaInstallPolicy";
26
+ import { BES_CONTINUE_LOCKOUT_MS, DOWNLOAD_STUCK_TIMEOUT_MS, GLOBAL_OTA_TIMEOUT_MS, LEGACY_APK_COMPLETION_SETTLE_MS, LEGACY_BES_CONTINUE_LOCKOUT_MS, LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS, LEGACY_GLOBAL_OTA_TIMEOUT_MS, LEGACY_MTK_INSTALL_TIMEOUT_MS, LEGACY_MTK_SIM_CAP_PERCENT, LEGACY_MTK_SIM_FLOOR_PERCENT, LEGACY_MTK_SIM_TICK_MS, LEGACY_MTK_STALL_DETECT_MS, LEGACY_MTK_STALL_ZONE_MAX_PERCENT, LEGACY_MTK_STALL_ZONE_MIN_PERCENT, LEGACY_PING_INTERVAL_MS, LEGACY_PROGRESS_TIMEOUT_MS, LEGACY_RETRY_INTERVAL_MS, MAX_RETRIES, MTK_INSTALL_TIMEOUT_MS, OtaProgressMessages, PING_INTERVAL_MS, PROGRESS_TIMEOUT_MS, QUERY_REPLY_TIMEOUT_MS, RETRY_INTERVAL_MS, isLegacyShapedOtaSession, isLegacyShapedOtaStatus, selectOtaProtocolProfile, } from "./otaInstallPolicy";
29
27
  function isTerminalForWatchdog(d) {
30
28
  return d === "complete" || d === "failed" || d === "restarting";
31
29
  }
30
+ function hotspotPreflightErrorMessage(error) {
31
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
32
+ switch (code) {
33
+ case "artifact_download_failed":
34
+ return OtaProgressMessages.hotspotArtifactDownloadFailed;
35
+ case "artifact_verify_failed":
36
+ case "manifest_invalid":
37
+ return OtaProgressMessages.hotspotArtifactVerifyFailed;
38
+ case "hotspot_wifi_permission_denied":
39
+ return OtaProgressMessages.hotspotWifiPermissionDenied;
40
+ case "hotspot_start_failed":
41
+ return OtaProgressMessages.hotspotStartFailed;
42
+ case "hotspot_join_failed":
43
+ return OtaProgressMessages.hotspotJoinFailed;
44
+ case "hotspot_server_failed":
45
+ return OtaProgressMessages.hotspotServerFailed;
46
+ default:
47
+ return OtaProgressMessages.hotspotServerFailed;
48
+ }
49
+ }
32
50
  /** Non-empty when glasses are actively reporting work — drives stall timeout reset. */
33
51
  function buildProgressStalenessSignature(otaStatus, otaProgress, displayState) {
34
52
  if (displayState !== "starting" && displayState !== "updating")
@@ -80,6 +98,11 @@ function hasRecoveringOtaReply(otaStatus, otaProgress) {
80
98
  }
81
99
  class OtaInstallCoordinator {
82
100
  attached = false;
101
+ preparedCheckResult = null;
102
+ selectedTransport = "wifi";
103
+ hotspotManifestUrl = null;
104
+ hotspotPhase = "idle";
105
+ hotspotArtifactPercent = null;
83
106
  // Genuinely session-local state (was component state/refs).
84
107
  errorMsg = "";
85
108
  sawReconnectEdge = false;
@@ -97,6 +120,28 @@ class OtaInstallCoordinator {
97
120
  apkExpectedInSession = false;
98
121
  // Latched when the legacy build-number completion fires; projected as "complete".
99
122
  apkCompletedViaBuildIncrease = false;
123
+ // The split routes selected legacy/unified behavior before navigation. Keep
124
+ // that choice sticky while an APK step upgrades ASG mid-flow.
125
+ protocolProfile = null;
126
+ // BES reboot recovery. Native bridges normalize explicit BES success to a
127
+ // terminal status; pre-37 flows additionally retain the legacy screen's
128
+ // lost-FINISHED fallback. Completion still requires a BES-specific observed
129
+ // disconnect -> reconnect lifecycle, never the generic session reconnect flag.
130
+ besRestartExpected = false;
131
+ besRestartDisconnectObserved = false;
132
+ besRestartRecovery = null;
133
+ // Downgrade detour (version-change mode). Captured at attach from the selected
134
+ // update: the pinned target is a LOWER build, executed by the recovery worker via
135
+ // uninstall->reinstall, so the phone must NOT drive ota_start on the intermediate
136
+ // reconnect (the passive factory build) and completion is exact-version
137
+ // convergence, not a build-number increase.
138
+ versionChangeSession = false;
139
+ versionChangeTarget = null;
140
+ // Set once ASG reports the apk install has started (the handoff point after which
141
+ // ASG dies and the recovery worker owns the transaction).
142
+ versionChangeInstallStarted = false;
143
+ // Latched when a reconnect reports buildNumber === versionChangeTarget.
144
+ versionChangeConverged = false;
100
145
  // Holds a legacy apk install FINISHED out of "complete" for the settle window.
101
146
  legacyApkSettleHold = false;
102
147
  // Display-only legacy MTK install stall simulation.
@@ -108,15 +153,20 @@ class OtaInstallCoordinator {
108
153
  retryTimeout = null;
109
154
  stuckTimeout = null;
110
155
  progressTimeout = null;
111
- postApkDelay = null;
112
156
  pingInterval = null;
113
157
  queryReplyTimeout = null;
114
158
  continueLockoutTimer = null;
115
159
  legacyApkSettleTimer = null;
116
160
  mtkStallDetectTimer = null;
117
161
  mtkSimTickTimer = null;
118
- // Retry / ack bookkeeping (no glasses source)
119
- hasReceivedAck = false;
162
+ // Request / retry bookkeeping (no glasses source)
163
+ // At most one apk-install status poll in flight (native rejects concurrent queries).
164
+ apkInstallPollInFlight = false;
165
+ // The native SDK owns one ota_start request until its application-level ack or
166
+ // timeout. Keep pending ownership across attach/detach, and retain the settled
167
+ // outcome while attached so Retry/query fallback cannot reinterpret an accepted
168
+ // command as permission to start a second OTA transaction.
169
+ otaStartOwnership = null;
120
170
  hasFirstActivity = false;
121
171
  // Stuck-at-zero watchdog clears only on first NON-ZERO progress; "first activity"
122
172
  // alone (e.g. ota_status with stepPercent=0) used to clear it too eagerly.
@@ -138,6 +188,24 @@ class OtaInstallCoordinator {
138
188
  storeUnsubscribe = null;
139
189
  snapshotCheckers = new Set();
140
190
  // --- public surface (engine.ota.installSession) ---
191
+ /** Select the transport at the existing install entry point before the progress route attaches. */
192
+ prepare(checkResult) {
193
+ const state = useGlassesStore.getState();
194
+ if (!state.wifiStatusKnown) {
195
+ throw new Error("Glasses Wi-Fi status is not available");
196
+ }
197
+ const hotspotSupported = state.hotspotOtaVersion === 1;
198
+ const wifiConnected = state.wifi.state === "connected";
199
+ if (!wifiConnected && !hotspotSupported) {
200
+ throw new Error("Connected glasses require Wi-Fi for OTA");
201
+ }
202
+ this.preparedCheckResult = checkResult;
203
+ this.selectedTransport = wifiConnected ? "wifi" : "hotspot";
204
+ this.hotspotManifestUrl = null;
205
+ this.hotspotPhase = "idle";
206
+ this.hotspotArtifactPercent = null;
207
+ return this.selectedTransport;
208
+ }
141
209
  /**
142
210
  * Bind the state machine to a mounted progress screen. Idempotent per
143
211
  * attach/detach cycle. Runs the initial-mount arbitration (send ota_start
@@ -149,6 +217,14 @@ class OtaInstallCoordinator {
149
217
  return;
150
218
  this.attached = true;
151
219
  this.resetSessionState();
220
+ const initialState = useGlassesStore.getState();
221
+ this.protocolProfile = selectOtaProtocolProfile(initialState.otaStatus, initialState.otaProgress, initialState.buildNumber);
222
+ const selected = initialState.otaUpdateAvailable;
223
+ if (selected?.isDowngrade && typeof selected.versionCode === "number" && selected.versionCode > 0) {
224
+ this.versionChangeSession = true;
225
+ this.versionChangeTarget = selected.versionCode;
226
+ console.log(`[OTA_PROGRESS] version-change (downgrade) session, target=${selected.versionCode}`);
227
+ }
152
228
  this.prevConnected = isGlassesConnected(useGlassesStore.getState().connection);
153
229
  this.storeUnsubscribe = useGlassesStore.subscribe(() => this.react());
154
230
  // Mount pass: every reaction block runs once (like all effects on mount),
@@ -180,13 +256,63 @@ class OtaInstallCoordinator {
180
256
  }
181
257
  GlobalEventEmitter.off("ota_start_ack", this.handleAck);
182
258
  GlobalEventEmitter.off("mtk_update_complete", this.handleMtkComplete);
259
+ GlobalEventEmitter.off("glasses_session_changed", this.handleGlassesSessionChanged);
183
260
  this.clearAllOtaTimers();
184
261
  this.clearContinueLockoutTimer();
262
+ // Only native command ownership survives a remount. Once the command has
263
+ // settled, the glasses store/durable ota_status is the session authority.
264
+ if (this.otaStartOwnership?.outcome !== "pending") {
265
+ this.otaStartOwnership = null;
266
+ }
185
267
  this.resetSessionState();
186
268
  }
187
269
  /** Retry after a failure: clear state and re-send ota_start (if connected). */
188
270
  retry() {
189
- console.log("[OTA_PROGRESS] retry pressed, clearing state and re-sending ota_start");
271
+ // While the recovery worker owns the detour (apk install latched, not yet
272
+ // converged), the phone must not drive: an ota_start now would push the
273
+ // factory/surviving build into a parallel download-install, and a second
274
+ // handoff would begin()/REPLACE the live transaction under the running
275
+ // worker. Retry just re-enters the reconcile wait — convergence or the
276
+ // session timeout decides the real outcome.
277
+ if (this.isInVersionChangeDetour()) {
278
+ console.log("[OTA_PROGRESS] retry during version-change detour — re-entering wait (no ota_start)");
279
+ this.setErrorMsg("");
280
+ this.emitInternalChange();
281
+ return;
282
+ }
283
+ const store = useGlassesStore.getState();
284
+ const otaStatus = store.otaStatus;
285
+ const otaProgress = store.otaProgress;
286
+ const authoritativeFailure = otaStatus?.status === "failed" || otaProgress?.status === "FAILED";
287
+ const hasUnfinishedSession = (!!otaStatus &&
288
+ otaStatus.status !== "idle" &&
289
+ otaStatus.status !== "failed" &&
290
+ otaStatus.status !== "complete") ||
291
+ (!!otaProgress && otaProgress.status !== "FAILED" && otaProgress.status !== "FINISHED");
292
+ const shouldReconcile = this.otaStartOwnership?.outcome === "pending" ||
293
+ (!authoritativeFailure &&
294
+ (this.otaStartOwnership?.outcome === "acknowledged" || this.hasFirstActivity || hasUnfinishedSession));
295
+ if (shouldReconcile) {
296
+ console.log("[OTA_PROGRESS] retry pressed for an existing OTA session — reconciling without ota_start");
297
+ this.clearPerStepTimers();
298
+ this.clearLegacyApkSettleTimer();
299
+ this.clearMtkSimulationTimers();
300
+ this.clearContinueLockoutTimer();
301
+ this.setErrorMsg("");
302
+ this.maybeStartGlobalTimeout();
303
+ this.maybeArmStuckWatchdog();
304
+ if (isGlassesConnected(store.connection)) {
305
+ if (this.otaStartOwnership?.outcome === "pending") {
306
+ void this.sendOtaStartWithWatchdogs();
307
+ }
308
+ else {
309
+ void BluetoothSdk.sendOtaQueryStatus().catch(() => { });
310
+ this.armQueryReplyFallback("retry");
311
+ }
312
+ }
313
+ return;
314
+ }
315
+ console.log("[OTA_PROGRESS] retry pressed after a rejected or terminal attempt — starting a fresh ota_start");
190
316
  // Batch like the old event handler: React ran the whole handler (including
191
317
  // the ota_start send) before re-rendering + re-running effects.
192
318
  const wasReacting = this.reacting;
@@ -195,16 +321,17 @@ class OtaInstallCoordinator {
195
321
  this.clearPerStepTimers();
196
322
  this.clearLegacyApkSettleTimer();
197
323
  this.clearMtkSimulationTimers();
324
+ this.clearContinueLockoutTimer();
198
325
  this.retryCount = 0;
199
- this.hasFirstActivity = false;
200
- this.hasReceivedAck = false;
326
+ if (!this.prepareFreshOtaStartAttempt())
327
+ return;
328
+ this.resetBesRestartAttempt();
201
329
  this.setSawReconnectEdge(false);
202
330
  this.setErrorMsg("");
203
- this.setApkCompletedViaBuildIncrease(false);
331
+ this.setContinueButtonDisabled(false);
204
332
  this.setLegacyApkSettleHold(false);
205
333
  this.setMtkSimulatedPercent(null);
206
334
  this.lastRealMtkProgress = 0;
207
- const store = useGlassesStore.getState();
208
335
  store.setOtaProgress(null);
209
336
  store.setOtaStatus(null);
210
337
  if (isGlassesConnected(useGlassesStore.getState().connection)) {
@@ -223,11 +350,15 @@ class OtaInstallCoordinator {
223
350
  * clear the stale build number so the next check re-reads version_info.
224
351
  */
225
352
  finish() {
353
+ if (this.otaStartOwnership?.outcome !== "pending") {
354
+ this.otaStartOwnership = null;
355
+ }
226
356
  useGlassesStore.getState().setOtaUpdateAvailable(null);
227
357
  if (this.apkStepSeen) {
228
358
  BluetoothSdk.updateGlasses({ buildNumber: "" });
229
359
  useGlassesStore.getState().setGlassesInfo({ buildNumber: "" });
230
360
  }
361
+ void this.teardownHotspotTransport();
231
362
  }
232
363
  /**
233
364
  * Abandon an interrupted session (the super-mode skip): terminal cleanup
@@ -255,16 +386,43 @@ class OtaInstallCoordinator {
255
386
  sawReconnectEdge: this.sawReconnectEdge,
256
387
  legacyApkSettleHold: this.legacyApkSettleHold,
257
388
  apkCompletedViaBuildIncrease: this.apkCompletedViaBuildIncrease,
389
+ besRestartRecovery: this.besRestartRecovery,
390
+ versionChangeConverged: this.versionChangeConverged,
391
+ versionChangeSession: this.versionChangeSession,
258
392
  }),
259
393
  errorMsg: this.errorMsg,
260
- continueButtonDisabled: this.continueButtonDisabled,
394
+ // Keep Continue disabled while the expected BES reboot is still in flight.
395
+ continueButtonDisabled: this.continueButtonDisabled || this.besRestartRecovery === "awaiting",
261
396
  connected,
262
397
  // Copies: the snapshot must not hand callers mutable references into the store.
263
398
  otaStatus: otaStatus ? { ...otaStatus } : null,
264
399
  otaProgress: otaProgress ? { ...otaProgress } : null,
265
400
  mtkInstallStallSimulatedPercent: this.mtkSimulatedPercent,
401
+ isVersionChange: this.versionChangeSession,
402
+ versionChangeConverged: this.versionChangeConverged,
403
+ versionChangePhase: this.deriveVersionChangePhase(connected),
404
+ hotspotPhase: this.hotspotPhase,
405
+ hotspotArtifactPercent: this.hotspotArtifactPercent,
266
406
  };
267
407
  }
408
+ /**
409
+ * True while the recovery worker owns the downgrade transaction (after the apk install
410
+ * started, before convergence). During this window ASG is being uninstalled/reinstalled and
411
+ * cannot answer the phone, so the coordinator must not ping, poll ota_query_status, or arm the
412
+ * progress-stall watchdog — doing so drives the passive factory build and fails the session
413
+ * before the target reconnects. Completion is exact-version convergence, detected separately.
414
+ */
415
+ isInVersionChangeDetour() {
416
+ return this.versionChangeSession && this.versionChangeInstallStarted && !this.versionChangeConverged;
417
+ }
418
+ /** Downgrade-detour sub-phase for the progress narrative (see OtaInstallSnapshot). */
419
+ deriveVersionChangePhase(connected) {
420
+ if (!this.versionChangeSession || this.versionChangeConverged)
421
+ return null;
422
+ if (!this.versionChangeInstallStarted)
423
+ return "installing";
424
+ return connected ? "verifying" : "restarting";
425
+ }
268
426
  /**
269
427
  * Subscribe to install snapshot changes, deduped on the projected JSON.
270
428
  * Fires on glasses-store changes AND coordinator-internal state changes
@@ -297,10 +455,16 @@ class OtaInstallCoordinator {
297
455
  this.initialBuildNumber = null;
298
456
  this.apkExpectedInSession = false;
299
457
  this.apkCompletedViaBuildIncrease = false;
458
+ this.protocolProfile = null;
459
+ this.resetBesRestartAttempt();
460
+ this.versionChangeSession = false;
461
+ this.versionChangeTarget = null;
462
+ this.versionChangeInstallStarted = false;
463
+ this.versionChangeConverged = false;
300
464
  this.legacyApkSettleHold = false;
301
465
  this.mtkSimulatedPercent = null;
302
466
  this.lastRealMtkProgress = 0;
303
- this.hasReceivedAck = false;
467
+ this.apkInstallPollInFlight = false;
304
468
  this.hasFirstActivity = false;
305
469
  this.hasFirstNonZeroProgress = false;
306
470
  this.retryCount = 0;
@@ -314,6 +478,24 @@ class OtaInstallCoordinator {
314
478
  this.lastStallSig = "";
315
479
  this.reactQueued = false;
316
480
  }
481
+ /** Begin a genuinely new command attempt only after native ownership has settled. */
482
+ prepareFreshOtaStartAttempt() {
483
+ if (this.otaStartOwnership?.outcome === "pending") {
484
+ console.error("[OTA_PROGRESS] refused fresh ota_start while native still owns the previous request");
485
+ return false;
486
+ }
487
+ this.otaStartOwnership = null;
488
+ this.hasFirstActivity = false;
489
+ this.hasFirstNonZeroProgress = false;
490
+ this.setApkCompletedViaBuildIncrease(false);
491
+ return true;
492
+ }
493
+ /** Reset only the reboot lifecycle that belongs to one install attempt. */
494
+ resetBesRestartAttempt() {
495
+ this.besRestartExpected = false;
496
+ this.besRestartDisconnectObserved = false;
497
+ this.besRestartRecovery = null;
498
+ }
317
499
  /** Notify snapshot subscribers of a coordinator-internal state change. */
318
500
  emitInternalChange() {
319
501
  for (const check of Array.from(this.snapshotCheckers))
@@ -363,14 +545,18 @@ class OtaInstallCoordinator {
363
545
  this.mtkSimulatedPercent = next;
364
546
  this.emitInternalChange();
365
547
  }
366
- /**
367
- * Legacy-session shape (WP 8C): event shape wins once events exist; before any event
368
- * only the session-start build number can tell (falls back to the live build number
369
- * until it is captured). Old builds get the LEGACY_* padded policy durations.
370
- */
548
+ /** Legacy/unified policy is selected once, matching the old sticky route choice. */
371
549
  isLegacySessionShapeNow() {
550
+ if (this.protocolProfile)
551
+ return this.protocolProfile === "legacy";
372
552
  const state = useGlassesStore.getState();
373
- return isLegacyShapedOtaSession(state.otaStatus, state.otaProgress, this.initialBuildNumber || state.buildNumber);
553
+ const selected = selectOtaProtocolProfile(state.otaStatus, state.otaProgress, this.initialBuildNumber || state.buildNumber);
554
+ if (selected) {
555
+ this.protocolProfile = selected;
556
+ console.log(`[OTA_PROGRESS] protocol profile selected: ${selected}`);
557
+ return selected === "legacy";
558
+ }
559
+ return isLegacyShapedOtaSession(state.otaStatus, state.otaProgress, state.buildNumber);
374
560
  }
375
561
  react() {
376
562
  if (!this.attached)
@@ -418,6 +604,18 @@ class OtaInstallCoordinator {
418
604
  this.handleLegacyInstallCompleteEdge(otaStatus);
419
605
  }
420
606
  const legacySession = this.isLegacySessionShapeNow();
607
+ if (this.isBesRestartExpected(otaStatus, otaProgress, legacySession)) {
608
+ this.besRestartExpected = true;
609
+ }
610
+ // Port the legacy route's actual reboot invariant before deriving display
611
+ // state, so reconnecting with stale BES in_progress@100 never renders updating.
612
+ if (connectedChanged && !connected && this.besRestartExpected && this.besRestartRecovery === null) {
613
+ this.besRestartDisconnectObserved = true;
614
+ this.besRestartRecovery = "awaiting";
615
+ console.log("[OTA_PROGRESS] BES restart disconnect observed — awaiting reconnect verification");
616
+ }
617
+ if (this.besRestartRecovery === "awaiting" && connected)
618
+ this.completeBesRestartRecovery();
421
619
  // Derived UI state — the glasses data IS the source of truth.
422
620
  const displayState = deriveDisplayState({
423
621
  otaStatus,
@@ -427,6 +625,9 @@ class OtaInstallCoordinator {
427
625
  sawReconnectEdge: this.sawReconnectEdge,
428
626
  legacyApkSettleHold: this.legacyApkSettleHold,
429
627
  apkCompletedViaBuildIncrease: this.apkCompletedViaBuildIncrease,
628
+ besRestartRecovery: this.besRestartRecovery,
629
+ versionChangeConverged: this.versionChangeConverged,
630
+ versionChangeSession: this.versionChangeSession,
430
631
  });
431
632
  const stallSig = buildProgressStalenessSignature(otaStatus, otaProgress, displayState);
432
633
  const stallDuration = progressTimeoutDurationMs(otaStatus, otaProgress, legacySession);
@@ -481,6 +682,62 @@ class OtaInstallCoordinator {
481
682
  console.log("[OTA_PROGRESS] legacy mtk install complete — marking MTK updated this session");
482
683
  useGlassesStore.getState().setMtkUpdatedThisSession(true);
483
684
  }
685
+ // Version-change (downgrade) install started: once ASG reports the apk install
686
+ // phase, the handoff to the recovery worker has happened. From here the phone
687
+ // stops driving ota_start (see runConnectEdge) and waits for exact-version
688
+ // convergence — the wipe means no resumable ASG session and no build increase.
689
+ if (this.versionChangeSession &&
690
+ !this.versionChangeInstallStarted &&
691
+ // Only latch from a status that belongs to THIS session: an install/apk status is a
692
+ // response to our own acknowledged ota_start, so a leftover from a prior session
693
+ // sitting in the store at mount cannot prematurely enter the detour wait (which would
694
+ // suppress ota_start and hang the fresh downgrade until the global timeout).
695
+ this.otaStartOwnership?.outcome === "acknowledged" &&
696
+ otaStatus?.stepType === "apk" &&
697
+ otaStatus.phase === "install") {
698
+ console.log("[OTA_PROGRESS] version-change: apk install started — entering detour wait");
699
+ this.versionChangeInstallStarted = true;
700
+ this.emitInternalChange();
701
+ }
702
+ // Ownership refuted: ASG emits install/STARTED BEFORE handing off, so the latch above can
703
+ // be set even when no transaction ends up owning the detour. Recovery answers every
704
+ // handoff synchronously, so exactly three error codes prove non-ownership and release the
705
+ // latch: downgrade_handoff_refused (recovery's explicit verdict),
706
+ // downgrade_handoff_failed (no verdict at all — recovery dead/missing, so it never began),
707
+ // and downgrade_transaction_stalled (the long-stop past recovery's own stale give-up).
708
+ // An accepted-but-slow transaction emits NONE of these (acceptance cancels the short
709
+ // watchdog), so the latch is never released while a live worker owns the staged artifact
710
+ // — which recovery additionally claims by rename at acceptance.
711
+ if (this.versionChangeInstallStarted &&
712
+ !this.versionChangeConverged &&
713
+ otaStatus?.status === "failed" &&
714
+ (otaStatus.error === "downgrade_handoff_refused" ||
715
+ otaStatus.error === "downgrade_handoff_failed" ||
716
+ otaStatus.error === "downgrade_transaction_stalled")) {
717
+ console.log(`[OTA_PROGRESS] version-change: no transaction owns the detour (${otaStatus.error}) — releasing latch`);
718
+ this.versionChangeInstallStarted = false;
719
+ this.emitInternalChange();
720
+ }
721
+ // Version-change convergence: the reconnected glasses report exactly the pinned
722
+ // target. This is the ONLY completion signal for a downgrade (lower build, no
723
+ // increase, wiped session). Mirrors the legacy build-increase latch but by equality.
724
+ if (this.versionChangeSession &&
725
+ this.versionChangeInstallStarted &&
726
+ !this.versionChangeConverged &&
727
+ this.versionChangeTarget !== null &&
728
+ buildNumberChanged &&
729
+ buildNumber) {
730
+ const currentBuild = Number.parseInt(buildNumber, 10);
731
+ if (Number.isFinite(currentBuild) && currentBuild === this.versionChangeTarget) {
732
+ console.log(`[OTA_PROGRESS] version-change converged: glasses report target ${currentBuild}, completing`);
733
+ this.clearPerStepTimers();
734
+ this.clearGlobalTimeout();
735
+ this.apkStepSeen = true;
736
+ this.setErrorMsg("");
737
+ this.versionChangeConverged = true;
738
+ this.emitInternalChange();
739
+ }
740
+ }
484
741
  // Legacy APK completion by build-number increase (WP 8C-c): old builds reboot into
485
742
  // the new build without a reliable explicit reconnect status; the fresh version_info
486
743
  // build number is the completion signal (ported from progress-legacy.tsx, which
@@ -516,7 +773,7 @@ class OtaInstallCoordinator {
516
773
  }
517
774
  /**
518
775
  * Connect-edge — the only place that decides to send ota_start /
519
- * ota_query_status, handles POST_APK delay, and flips sawReconnectEdge
776
+ * ota_query_status and flips sawReconnectEdge
520
777
  * on the false -> true transition that signals the BES reboot completed.
521
778
  */
522
779
  if (connectedChanged) {
@@ -528,21 +785,23 @@ class OtaInstallCoordinator {
528
785
  if (isMount) {
529
786
  GlobalEventEmitter.on("ota_start_ack", this.handleAck);
530
787
  GlobalEventEmitter.on("mtk_update_complete", this.handleMtkComplete);
788
+ GlobalEventEmitter.on("glasses_session_changed", this.handleGlassesSessionChanged);
531
789
  }
532
790
  // Ping keepalive while an OTA is actively running (legacy sessions pinged on the
533
791
  // padded interval — old builds slept less eagerly and pings are costlier for them).
534
792
  if (connectedChanged || displayStateChanged) {
535
793
  this.clearPingInterval();
536
- const active = connected && (displayState === "starting" || displayState === "updating");
794
+ const active = connected && (displayState === "starting" || displayState === "updating") && !this.isInVersionChangeDetour();
537
795
  if (active) {
538
796
  void BluetoothSdk.ping().catch(() => { });
539
797
  const pingIntervalMs = legacySession ? LEGACY_PING_INTERVAL_MS : PING_INTERVAL_MS;
540
798
  this.pingInterval = setInterval(() => {
541
799
  void BluetoothSdk.ping().catch(() => { });
800
+ this.maybePollApkInstallStatus();
542
801
  }, pingIntervalMs);
543
802
  }
544
803
  }
545
- // Any glasses activity (ota_status or otaProgress) clears the no-ack retry watchdog.
804
+ // Any glasses activity proves the logical ota_start was accepted.
546
805
  if ((otaStatusChanged || otaProgressChanged) && (otaStatus || otaProgress)) {
547
806
  this.onFirstActivity();
548
807
  }
@@ -563,7 +822,11 @@ class OtaInstallCoordinator {
563
822
  // update that doesn't change the signature keeps the same timer running
564
823
  // instead of forever re-extending the deadline.
565
824
  if (stallSigChanged || displayStateChanged) {
566
- if (displayState !== "starting" && displayState !== "updating") {
825
+ if (this.isInVersionChangeDetour()) {
826
+ // Recovery worker owns the transaction; there are no progress events to stall on.
827
+ this.clearProgressTimeout();
828
+ }
829
+ else if (displayState !== "starting" && displayState !== "updating") {
567
830
  this.clearProgressTimeout();
568
831
  }
569
832
  else if (!stallSig) {
@@ -583,19 +846,14 @@ class OtaInstallCoordinator {
583
846
  }
584
847
  // Lockout on Continue button after BES restart to prevent accidental tap
585
848
  // (15s unified, 35s for legacy-shaped sessions — WP 8C-f).
586
- if (displayStateChanged) {
849
+ if (displayStateChanged && displayState === "restarting") {
587
850
  this.clearContinueLockoutTimer();
588
- if (displayState === "restarting") {
589
- const lockoutMs = legacySession ? LEGACY_BES_CONTINUE_LOCKOUT_MS : BES_CONTINUE_LOCKOUT_MS;
590
- this.setContinueButtonDisabled(true);
591
- this.continueLockoutTimer = setTimeout(() => {
592
- this.continueLockoutTimer = null;
593
- this.setContinueButtonDisabled(false);
594
- }, lockoutMs);
595
- }
596
- }
597
- if (displayStateChanged && displayState === "failed") {
598
- this.clearPerStepTimers();
851
+ const lockoutMs = legacySession ? LEGACY_BES_CONTINUE_LOCKOUT_MS : BES_CONTINUE_LOCKOUT_MS;
852
+ this.setContinueButtonDisabled(true);
853
+ this.continueLockoutTimer = setTimeout(() => {
854
+ this.continueLockoutTimer = null;
855
+ this.setContinueButtonDisabled(false);
856
+ }, lockoutMs);
599
857
  }
600
858
  // Legacy MTK install stall simulation (WP 8C-e): display-only. The simulation
601
859
  // timers only ever touch the projected snapshot percent — they never feed back
@@ -609,69 +867,96 @@ class OtaInstallCoordinator {
609
867
  // completions (which never produce a `complete` ota_status).
610
868
  if (displayStateChanged &&
611
869
  (displayState === "complete" || displayState === "restarting" || displayState === "failed")) {
870
+ if (displayState === "failed") {
871
+ this.clearPerStepTimers();
872
+ }
612
873
  useGlassesStore.getState().setOtaUpdateAvailable(null);
874
+ if (displayState === "complete" || displayState === "failed") {
875
+ void this.teardownHotspotTransport();
876
+ }
877
+ }
878
+ }
879
+ /**
880
+ * Reconnect arbitration shared by the physical BLE connect edge and the
881
+ * glasses_session_changed signal. The latter exists because the BES keeps the BLE
882
+ * link alive across asg_client restarts (APK OTA): no physical edge ever fires, so
883
+ * the changed process session id is the only reconnect signal the coordinator gets
884
+ * (incident rep_01KY6BJ0B7A4RBMQ7VN39KAE5E). Always returns true (the caller's
885
+ * edge handling is complete).
886
+ */
887
+ runReconnectArbitration(label) {
888
+ console.log(`[OTA_PROGRESS] ${label}: false->true, flipping sawReconnectEdge=true`);
889
+ // A physical reconnect passed through the disconnect branch first, which cleared
890
+ // these timers; a session-change edge never disconnects, so a fallback armed by an
891
+ // earlier mount/query could otherwise fire alongside the arbitration below and send
892
+ // a duplicate ota_start. Clearing here is a no-op on the physical path.
893
+ this.clearQueryReplyTimeout();
894
+ this.setSawReconnectEdge(true);
895
+ if (this.besRestartDisconnectObserved) {
896
+ console.log(`[OTA_PROGRESS] ${label}: BES reboot reconnect observed — no ota_query_status`);
897
+ this.completeBesRestartRecovery();
898
+ return true;
899
+ }
900
+ // Legacy apk settle window (WP 8C): the glasses restarting into the new build is
901
+ // the EXPECTED reconnect here — the legacy screen took no action on it. Skip the
902
+ // query/fallback; the settle timer (or the build-number increase) completes.
903
+ if (this.legacyApkSettleHold) {
904
+ console.log(`[OTA_PROGRESS] ${label}: reconnect during legacy apk settle hold — no query`);
905
+ return true;
613
906
  }
907
+ console.log(`[OTA_PROGRESS] ${label}: reconnected, sending ota_query_status`);
908
+ if (this.otaStartOwnership?.outcome === "pending") {
909
+ void this.sendOtaStartWithWatchdogs();
910
+ }
911
+ void BluetoothSdk.sendOtaQueryStatus();
912
+ this.armQueryReplyFallback("reconnect");
913
+ return true;
614
914
  }
615
915
  runConnectEdge(connected) {
616
916
  const prev = this.prevConnected;
617
917
  this.prevConnected = connected;
618
918
  if (!connected) {
619
919
  console.log("[OTA_PROGRESS] connect-edge: disconnected");
620
- this.clearPostApkDelay();
621
- // Also drop a pending query-reply fallback: firing ota_start into a dead
622
- // link can only produce a false "failed" state, and the reconnect edge
623
- // re-queries and re-arms this fallback anyway.
920
+ // Also drop pending query/retry sends: firing ota_start into a dead link
921
+ // can only produce a false "failed" state. Reconnect arbitration first
922
+ // queries durable status and re-arms the one permitted fallback.
624
923
  this.clearQueryReplyTimeout();
924
+ this.clearRetryTimeout();
625
925
  return;
626
926
  }
627
927
  const becameConnected = prev === false && connected === true;
628
- if (becameConnected) {
629
- console.log("[OTA_PROGRESS] connect-edge: false->true, flipping sawReconnectEdge=true");
630
- this.setSawReconnectEdge(true);
631
- }
632
- // Legacy apk settle window (WP 8C): the glasses restarting into the new build is
633
- // the EXPECTED reconnect here — the legacy screen took no action on it. Skip the
634
- // query/fallback; the settle timer (or the build-number increase) completes.
635
- if (becameConnected && this.legacyApkSettleHold) {
636
- console.log("[OTA_PROGRESS] connect-edge: reconnect during legacy apk settle hold — no query");
637
- return;
638
- }
639
- const storeState = useGlassesStore.getState();
640
- const s = storeState.otaStatus;
641
- const postApkAwaiting = s?.stepType === "apk" && (s?.totalSteps ?? 0) > 1 && (s.status === "step_complete" || s.status === "complete");
642
- if (becameConnected && postApkAwaiting) {
643
- console.log("[OTA_PROGRESS] connect-edge: post-APK reboot detected, arming POST_APK delay for sendOtaStart");
644
- this.clearPostApkDelay();
645
- this.postApkDelay = setTimeout(() => {
646
- this.postApkDelay = null;
647
- this.retryCount = 0;
648
- this.hasFirstActivity = false;
649
- this.hasFirstNonZeroProgress = false;
650
- this.hasReceivedAck = false;
651
- console.log("[OTA_PROGRESS] POST_APK delay fired, sending ota_start");
652
- void this.sendOtaStartWithWatchdogs();
653
- }, POST_APK_OTA_START_DELAY_MS);
928
+ if (becameConnected && this.runReconnectArbitration("connect-edge")) {
654
929
  return;
655
930
  }
656
- if (becameConnected) {
657
- console.log("[OTA_PROGRESS] connect-edge: reconnected, sending ota_query_status");
658
- void BluetoothSdk.sendOtaQueryStatus();
659
- this.armQueryReplyFallback("reconnect");
931
+ // Version-change detour: after the install started, EVERY reconnect is expected
932
+ // (the passive factory build, then the target). Do NOT send ota_start/query — the
933
+ // factory build would treat the pin as a plain upgrade and start a redundant
934
+ // download racing the recovery worker. Convergence (buildNumber === target) is
935
+ // detected in the reaction pass; here we simply take no action.
936
+ if (this.versionChangeSession && this.versionChangeInstallStarted && !this.versionChangeConverged) {
937
+ console.log("[OTA_PROGRESS] connect-edge: reconnect during version-change detour — no query (expected)");
660
938
  return;
661
939
  }
940
+ const storeState = useGlassesStore.getState();
662
941
  // Initial mount (prev === current === true). If no session yet, kick off ota_start.
663
942
  // An idle ota_status has an empty sessionId; treat it as no session so ota_start
664
943
  // still begins an explicit install.
665
944
  const isIdleStatus = !!storeState.otaStatus && storeState.otaStatus.sessionId === "";
666
945
  const noSessionYet = (!storeState.otaStatus && !storeState.otaProgress) || isIdleStatus;
667
946
  if (noSessionYet) {
947
+ if (!isIdleStatus && this.otaStartOwnership?.outcome === "acknowledged") {
948
+ console.log("[OTA_PROGRESS] initial mount, acknowledged session has no cached status — reconciling");
949
+ void BluetoothSdk.sendOtaQueryStatus().catch(() => { });
950
+ this.armQueryReplyFallback("initial-mount");
951
+ return;
952
+ }
668
953
  console.log(isIdleStatus
669
954
  ? "[OTA_PROGRESS] initial mount, idle status (no sessionId), sending ota_start"
670
955
  : "[OTA_PROGRESS] initial mount, no session in store, sending ota_start");
671
956
  this.retryCount = 0;
672
- this.hasFirstActivity = false;
673
- this.hasFirstNonZeroProgress = false;
674
- this.hasReceivedAck = false;
957
+ if (isIdleStatus && this.otaStartOwnership?.outcome !== "pending" && !this.prepareFreshOtaStartAttempt()) {
958
+ return;
959
+ }
675
960
  void this.sendOtaStartWithWatchdogs();
676
961
  }
677
962
  else {
@@ -682,12 +967,22 @@ class OtaInstallCoordinator {
682
967
  }
683
968
  // --- BLE OTA event listeners (engine GlobalEventEmitter, fed by OtaService) ---
684
969
  handleAck = () => {
685
- if (this.hasReceivedAck)
970
+ const ownership = this.otaStartOwnership;
971
+ if (!ownership || ownership.outcome === "rejected")
972
+ return;
973
+ if (ownership.outcome === "acknowledged")
686
974
  return;
687
975
  console.log("[OTA_PROGRESS] ota_start_ack received");
688
- this.hasReceivedAck = true;
976
+ ownership.outcome = "acknowledged";
689
977
  this.clearRetryTimeout();
690
978
  };
979
+ /**
980
+ * The asg process restarted while the BES kept the BLE link up (sid change observed
981
+ * by the bridge): treat it exactly like a physical reconnect edge.
982
+ */
983
+ handleGlassesSessionChanged = () => {
984
+ this.runReconnectArbitration("session-change");
985
+ };
691
986
  handleMtkComplete = () => {
692
987
  console.log("[OTA_PROGRESS] mtk_update_complete received");
693
988
  this.clearProgressTimeout();
@@ -732,6 +1027,34 @@ class OtaInstallCoordinator {
732
1027
  }, LEGACY_APK_COMPLETION_SETTLE_MS);
733
1028
  }
734
1029
  }
1030
+ isBesRestartExpected(otaStatus, otaProgress, legacySession) {
1031
+ const statusReadyToRestart = otaStatus?.stepType === "bes" &&
1032
+ otaStatus.phase === "install" &&
1033
+ (otaStatus.status === "step_complete" || otaStatus.status === "complete");
1034
+ const progressReadyToRestart = legacySession &&
1035
+ otaProgress?.currentUpdate === "bes" &&
1036
+ otaProgress.stage === "install" &&
1037
+ otaProgress.status === "FINISHED";
1038
+ // progress-legacy treated a BES disconnect during active installation as the
1039
+ // expected power cycle even when its terminal FINISHED event was lost. Keep
1040
+ // that fallback only for the sticky pre-37 profile selected when the flow began.
1041
+ const legacyInstallInProgress = legacySession &&
1042
+ ((otaStatus?.stepType === "bes" && otaStatus.phase === "install" && otaStatus.status === "in_progress") ||
1043
+ (otaProgress?.currentUpdate === "bes" &&
1044
+ otaProgress.stage === "install" &&
1045
+ (otaProgress.status === "STARTED" || otaProgress.status === "PROGRESS")));
1046
+ return statusReadyToRestart || progressReadyToRestart || legacyInstallInProgress;
1047
+ }
1048
+ /** Complete only after a protocol-authorized BES restart traverses disconnect -> reconnect. */
1049
+ completeBesRestartRecovery() {
1050
+ if (!this.besRestartDisconnectObserved || this.besRestartRecovery !== "awaiting")
1051
+ return;
1052
+ console.log("[OTA_PROGRESS] BES restart reconnected — completing by reboot edge");
1053
+ this.clearPerStepTimers();
1054
+ this.setErrorMsg("");
1055
+ this.besRestartRecovery = "complete";
1056
+ this.emitInternalChange();
1057
+ }
735
1058
  /**
736
1059
  * Legacy MTK install stall simulation (WP 8C-e), ported verbatim: the MTK system
737
1060
  * install typically stalls around 49-50%. Every real event restarts stall detection;
@@ -795,10 +1118,45 @@ class OtaInstallCoordinator {
795
1118
  sawReconnectEdge: this.sawReconnectEdge,
796
1119
  legacyApkSettleHold: this.legacyApkSettleHold,
797
1120
  apkCompletedViaBuildIncrease: this.apkCompletedViaBuildIncrease,
1121
+ besRestartRecovery: this.besRestartRecovery,
1122
+ versionChangeConverged: this.versionChangeConverged,
1123
+ versionChangeSession: this.versionChangeSession,
798
1124
  });
799
1125
  }
800
1126
  /**
801
- * "Glasses are talking to us" clears the no-ack retry watchdog only.
1127
+ * During an APK install the package installer kills and restarts the glasses
1128
+ * process, and the new process's completion push can lose its startup race
1129
+ * against the UART transport (incident rep_01KY31HEMTSBSMK8DVMNXJ5XGG: the
1130
+ * phone sat on a stale "install in_progress 0%" until the stall watchdog
1131
+ * failed a successful update). The persisted session on the glasses always
1132
+ * knows the truth, so poll ota_query_status on the keepalive tick while an
1133
+ * apk step sits in its install phase — the reply either confirms progress or
1134
+ * delivers the missed completion. Gated to the apk install phase so a stray
1135
+ * idle reply can't disturb an active download; legacy (< 37) builds ignore
1136
+ * the query, which is harmless.
1137
+ *
1138
+ * The native query pends up to 15s and rejects a concurrent call with
1139
+ * request_in_flight — longer than the 10s tick — so exactly one poll is kept
1140
+ * in flight (the glasses being slow to answer IS the silent-restart window
1141
+ * this poll exists for) and rejections are swallowed: the next tick retries.
1142
+ */
1143
+ maybePollApkInstallStatus() {
1144
+ if (this.isInVersionChangeDetour())
1145
+ return;
1146
+ if (this.apkInstallPollInFlight)
1147
+ return;
1148
+ const s = useGlassesStore.getState().otaStatus;
1149
+ if (s?.stepType === "apk" && s.phase === "install" && s.status === "in_progress") {
1150
+ this.apkInstallPollInFlight = true;
1151
+ void BluetoothSdk.sendOtaQueryStatus()
1152
+ .catch(() => { })
1153
+ .finally(() => {
1154
+ this.apkInstallPollInFlight = false;
1155
+ });
1156
+ }
1157
+ }
1158
+ /**
1159
+ * "Glasses are talking to us" — suppresses retry if the native request later rejects.
802
1160
  * Important: this does NOT clear the stuck-at-zero watchdog; that one fires
803
1161
  * on real progress > 0% (see {@link onFirstNonZeroProgress}).
804
1162
  */
@@ -806,6 +1164,9 @@ class OtaInstallCoordinator {
806
1164
  if (this.hasFirstActivity)
807
1165
  return;
808
1166
  this.hasFirstActivity = true;
1167
+ if (this.otaStartOwnership?.outcome === "pending") {
1168
+ this.otaStartOwnership.outcome = "acknowledged";
1169
+ }
809
1170
  this.clearRetryTimeout();
810
1171
  }
811
1172
  /**
@@ -828,7 +1189,7 @@ class OtaInstallCoordinator {
828
1189
  // Legacy-shape check happens once, when the session cap is armed at the first
829
1190
  // ota_start send (WP 8C-b): old builds get the padded legacy cap for the whole
830
1191
  // session; a session that starts unified keeps the unified cap.
831
- const globalTimeoutMs = this.isLegacySessionShapeNow() ? LEGACY_GLOBAL_OTA_TIMEOUT_MS : GLOBAL_OTA_TIMEOUT_MS;
1192
+ const globalTimeoutMs = this.versionChangeSession || this.isLegacySessionShapeNow() ? LEGACY_GLOBAL_OTA_TIMEOUT_MS : GLOBAL_OTA_TIMEOUT_MS;
832
1193
  this.globalTimeout = setTimeout(() => {
833
1194
  this.globalTimeout = null;
834
1195
  this.globalTimeoutStarted = false;
@@ -844,35 +1205,12 @@ class OtaInstallCoordinator {
844
1205
  this.setErrorMsg(OtaProgressMessages.globalTimeout);
845
1206
  }, globalTimeoutMs);
846
1207
  }
847
- armAckAndStuckWatchdogsOnly() {
848
- this.clearRetryTimeout();
1208
+ armStuckWatchdogOnly() {
849
1209
  this.clearStuckTimeout();
850
1210
  // Durations resolved at arm time (WP 8C-b): legacy-shaped sessions get the
851
1211
  // padded values progress-legacy.tsx used for old (< 37) builds.
852
1212
  const legacySession = this.isLegacySessionShapeNow();
853
- const retryIntervalMs = legacySession ? LEGACY_RETRY_INTERVAL_MS : RETRY_INTERVAL_MS;
854
1213
  const stuckTimeoutMs = legacySession ? LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS : DOWNLOAD_STUCK_TIMEOUT_MS;
855
- this.retryTimeout = setTimeout(() => {
856
- this.retryTimeout = null;
857
- if (this.hasReceivedAck || this.hasFirstActivity)
858
- return;
859
- if (this.computeDisplayStateNow() !== "starting")
860
- return;
861
- if (this.retryCount < MAX_RETRIES - 1) {
862
- this.retryCount += 1;
863
- this.hasReceivedAck = false;
864
- console.log(`[OTA_PROGRESS] watchdog: no ack in ${retryIntervalMs}ms, retrying ota_start (attempt ${this.retryCount})`);
865
- void this.sendOtaStartWithWatchdogs(true)
866
- .then(() => {
867
- this.armAckAndStuckWatchdogsOnly();
868
- })
869
- .catch(() => { });
870
- }
871
- else {
872
- console.log(`[OTA_PROGRESS] watchdog: ota_start ack never received after ${MAX_RETRIES} attempts, failing`);
873
- this.setErrorMsg(OtaProgressMessages.noAckResponse);
874
- }
875
- }, retryIntervalMs);
876
1214
  this.stuckTimeout = setTimeout(() => {
877
1215
  this.stuckTimeout = null;
878
1216
  const d = this.computeDisplayStateNow();
@@ -888,27 +1226,120 @@ class OtaInstallCoordinator {
888
1226
  this.setErrorMsg(OtaProgressMessages.stalledOrStuck);
889
1227
  }, stuckTimeoutMs);
890
1228
  }
891
- async sendOtaStartWithWatchdogs(retryAlreadyCounted = false) {
1229
+ /** Arm the 0%-stuck watchdog only when this logical session has none. */
1230
+ maybeArmStuckWatchdog() {
1231
+ if (this.stuckTimeout || this.hasFirstNonZeroProgress)
1232
+ return;
1233
+ this.armStuckWatchdogOnly();
1234
+ }
1235
+ sendOtaStartWithWatchdogs() {
1236
+ // INVARIANT BACKSTOP — not normal control flow. Every entry point that can drive the
1237
+ // glasses (connect-edge, query fallback, retry, mount) carries its own detour gate with the
1238
+ // right behavior for that site; this final check only exists so that a FUTURE entry point
1239
+ // that forgets its gate degrades to a refused send + loud log instead of a corrupted
1240
+ // transaction (an ota_start mid-detour starts a parallel install on the factory build and
1241
+ // can trigger a second handoff). If this ever fires, a missed site gate exists — fix it.
1242
+ if (this.isInVersionChangeDetour()) {
1243
+ console.error("[OTA_PROGRESS] BACKSTOP: refused ota_start during a latched version-change detour — a caller is missing its detour gate");
1244
+ return Promise.resolve();
1245
+ }
1246
+ const existingOwnership = this.otaStartOwnership;
1247
+ if (existingOwnership?.outcome === "pending") {
1248
+ this.maybeStartGlobalTimeout();
1249
+ if (this.selectedTransport === "wifi" || this.hotspotManifestUrl)
1250
+ this.maybeArmStuckWatchdog();
1251
+ console.log("[OTA_PROGRESS] ota_start already pending — adopting existing native request");
1252
+ return existingOwnership.promise;
1253
+ }
1254
+ if (existingOwnership?.outcome === "acknowledged") {
1255
+ this.maybeStartGlobalTimeout();
1256
+ this.maybeArmStuckWatchdog();
1257
+ console.log("[OTA_PROGRESS] ota_start already acknowledged — refusing duplicate start");
1258
+ return Promise.resolve();
1259
+ }
1260
+ if (existingOwnership?.outcome === "rejected" && !this.prepareFreshOtaStartAttempt()) {
1261
+ return Promise.resolve();
1262
+ }
892
1263
  this.maybeStartGlobalTimeout();
893
- this.hasReceivedAck = false;
894
- this.armAckAndStuckWatchdogsOnly();
1264
+ if (this.selectedTransport === "wifi" || this.hotspotManifestUrl)
1265
+ this.maybeArmStuckWatchdog();
1266
+ const ownership = {
1267
+ outcome: "pending",
1268
+ promise: Promise.resolve(),
1269
+ };
1270
+ ownership.promise = this.performOtaStart(ownership);
1271
+ this.otaStartOwnership = ownership;
1272
+ return ownership.promise;
1273
+ }
1274
+ async performOtaStart(ownership) {
1275
+ let nativeStartAttempted = false;
895
1276
  try {
896
1277
  const state = useGlassesStore.getState();
897
- const otaVersionUrl = getAsgOtaVersionUrl(state.otaVersionUrl, state.buildNumber);
898
- console.log(`[OTA_PROGRESS] sending ota_start with manifest URL: ${otaVersionUrl}`);
1278
+ let otaVersionUrl = resolveOtaManifestUrl(state.otaVersionUrl, state.buildNumber);
1279
+ if (this.selectedTransport === "hotspot") {
1280
+ if (!this.hotspotManifestUrl) {
1281
+ if (!this.preparedCheckResult) {
1282
+ throw new Error("No selected OTA check is available for hotspot staging");
1283
+ }
1284
+ this.hotspotManifestUrl = await hotspotOtaTransport.prepare(this.preparedCheckResult, (progress) => {
1285
+ this.hotspotPhase = progress.phase;
1286
+ this.hotspotArtifactPercent = progress.artifact?.artifactPercent ?? null;
1287
+ this.emitInternalChange();
1288
+ });
1289
+ }
1290
+ otaVersionUrl = this.hotspotManifestUrl;
1291
+ this.maybeArmStuckWatchdog();
1292
+ }
1293
+ console.log(`[OTA_PROGRESS] sending ota_start with ${this.selectedTransport} manifest URL: ${otaVersionUrl}`);
1294
+ nativeStartAttempted = true;
899
1295
  await BluetoothSdk.startOtaUpdate(otaVersionUrl);
1296
+ if (this.otaStartOwnership !== ownership)
1297
+ return;
1298
+ // The public SDK promise resolves only from ota_start_ack. Treat that
1299
+ // resolution as the acknowledgement even if the parallel event dispatch
1300
+ // reaches the coordinator later (or is dropped by a remount).
1301
+ this.handleAck();
900
1302
  }
901
1303
  catch (err) {
902
1304
  console.warn("[OTA_PROGRESS] sendOtaStart threw", err);
903
- this.clearRetryTimeout();
1305
+ if (this.otaStartOwnership !== ownership)
1306
+ return;
1307
+ if (this.selectedTransport === "hotspot" && !nativeStartAttempted) {
1308
+ ownership.outcome = "rejected";
1309
+ this.clearStuckTimeout();
1310
+ await this.teardownHotspotTransport();
1311
+ if (this.attached)
1312
+ this.setErrorMsg(hotspotPreflightErrorMessage(err));
1313
+ return;
1314
+ }
1315
+ // A received ota_status/legacy progress event, or the exact legacy APK
1316
+ // build-number proof, proves that this logical start was accepted. Its
1317
+ // application-level ack may be lost while the update continues; never
1318
+ // create another transaction or fail a live, restarting, or completed
1319
+ // session because that stale promise timed out.
1320
+ if (this.hasFirstActivity || this.apkCompletedViaBuildIncrease) {
1321
+ ownership.outcome = "acknowledged";
1322
+ console.log("[OTA_PROGRESS] ota_start rejection arrived after acceptance proof — ignoring stale rejection");
1323
+ return;
1324
+ }
1325
+ ownership.outcome = "rejected";
1326
+ if (!this.attached)
1327
+ return;
904
1328
  this.clearStuckTimeout();
1329
+ if (!isGlassesConnected(useGlassesStore.getState().connection)) {
1330
+ console.log("[OTA_PROGRESS] ota_start request ended while disconnected; deferring retry to reconnect arbitration");
1331
+ return;
1332
+ }
905
1333
  if (this.retryCount < MAX_RETRIES - 1) {
906
- if (!retryAlreadyCounted) {
907
- this.retryCount += 1;
908
- }
1334
+ this.retryCount += 1;
909
1335
  const retryIntervalMs = this.isLegacySessionShapeNow() ? LEGACY_RETRY_INTERVAL_MS : RETRY_INTERVAL_MS;
1336
+ console.log(`[OTA_PROGRESS] ota_start request ended without ack or activity; retrying after ${retryIntervalMs}ms (attempt ${this.retryCount + 1}/${MAX_RETRIES})`);
910
1337
  this.retryTimeout = setTimeout(() => {
911
1338
  this.retryTimeout = null;
1339
+ if (!isGlassesConnected(useGlassesStore.getState().connection)) {
1340
+ console.log("[OTA_PROGRESS] ota_start retry paused while disconnected; reconnect arbitration will resume");
1341
+ return;
1342
+ }
912
1343
  void this.sendOtaStartWithWatchdogs();
913
1344
  }, retryIntervalMs);
914
1345
  }
@@ -918,12 +1349,22 @@ class OtaInstallCoordinator {
918
1349
  }
919
1350
  }
920
1351
  }
1352
+ async teardownHotspotTransport() {
1353
+ if (this.selectedTransport !== "hotspot" && !this.hotspotManifestUrl)
1354
+ return;
1355
+ await hotspotOtaTransport.teardown();
1356
+ this.hotspotManifestUrl = null;
1357
+ this.hotspotPhase = "idle";
1358
+ this.hotspotArtifactPercent = null;
1359
+ this.emitInternalChange();
1360
+ }
921
1361
  /**
922
1362
  * After sending ota_query_status, wait QUERY_REPLY_TIMEOUT_MS for the glasses
923
- * to reply with a useful ota_status. If nothing arrives (e.g. the glasses'
924
- * OTA session was wiped between mount and reconnect), or the glasses reply
925
- * with an explicit "idle" status (no session), fall back to ota_start so the
926
- * user doesn't sit on a spinner forever.
1363
+ * to reply with a useful ota_status. A unified session may start again only
1364
+ * after an explicit "idle" reply, or after a locally rejected ota_start has
1365
+ * produced no activity and no authoritative reply across reconnect. Legacy
1366
+ * builds ignore ota_query_status, so their established timeout fallback
1367
+ * remains the compatibility path.
927
1368
  *
928
1369
  * Cleared as soon as a non-idle otaStatus or any otaProgress lands in the
929
1370
  * store (see the reaction block above). An idle reply intentionally does NOT
@@ -940,17 +1381,27 @@ class OtaInstallCoordinator {
940
1381
  // whatever legacy-shaped status/progress sits in the store is a pre-query
941
1382
  // leftover, not a reply — a NEW legacy event would have cleared this timer
942
1383
  // through the reaction pass. Only a unified reply suppresses the fallback.
943
- const legacySession = isLegacyShapedOtaSession(state.otaStatus, state.otaProgress, this.initialBuildNumber || state.buildNumber);
1384
+ const legacySession = this.isLegacySessionShapeNow();
944
1385
  if (!legacySession && hasRecoveringOtaReply(state.otaStatus, state.otaProgress))
945
1386
  return;
1387
+ const explicitIdle = state.otaStatus?.status === "idle";
1388
+ const rejectedStart = this.otaStartOwnership?.outcome === "rejected";
1389
+ if (!legacySession && !explicitIdle && !rejectedStart) {
1390
+ console.log(`[OTA_PROGRESS] watchdog: ota_query_status got no authoritative idle reply in ${QUERY_REPLY_TIMEOUT_MS}ms (reason=${reason}); preserving the existing session`);
1391
+ return;
1392
+ }
946
1393
  // Don't fire if we've left the active phase (e.g. user backed out, error overlay).
947
1394
  if (isTerminalForWatchdog(this.computeDisplayStateNow()))
948
1395
  return;
949
- console.log(`[OTA_PROGRESS] watchdog: ota_query_status got no useful reply in ${QUERY_REPLY_TIMEOUT_MS}ms (reason=${reason}), falling back to ota_start`);
1396
+ let fallbackCause = "legacy query timeout";
1397
+ if (explicitIdle)
1398
+ fallbackCause = "idle ota_status";
1399
+ else if (rejectedStart)
1400
+ fallbackCause = "rejected ota_start with no recovery reply";
1401
+ console.log(`[OTA_PROGRESS] watchdog: ${fallbackCause} (reason=${reason}), falling back to ota_start`);
950
1402
  this.retryCount = 0;
951
- this.hasFirstActivity = false;
952
- this.hasFirstNonZeroProgress = false;
953
- this.hasReceivedAck = false;
1403
+ if (this.otaStartOwnership?.outcome !== "pending" && !this.prepareFreshOtaStartAttempt())
1404
+ return;
954
1405
  void this.sendOtaStartWithWatchdogs();
955
1406
  }, QUERY_REPLY_TIMEOUT_MS);
956
1407
  }
@@ -980,12 +1431,6 @@ class OtaInstallCoordinator {
980
1431
  }
981
1432
  this.globalTimeoutStarted = false;
982
1433
  }
983
- clearPostApkDelay() {
984
- if (this.postApkDelay) {
985
- clearTimeout(this.postApkDelay);
986
- this.postApkDelay = null;
987
- }
988
- }
989
1434
  clearPingInterval() {
990
1435
  if (this.pingInterval) {
991
1436
  clearInterval(this.pingInterval);
@@ -1031,7 +1476,6 @@ class OtaInstallCoordinator {
1031
1476
  this.clearRetryTimeout();
1032
1477
  this.clearStuckTimeout();
1033
1478
  this.clearProgressTimeout();
1034
- this.clearPostApkDelay();
1035
1479
  this.clearQueryReplyTimeout();
1036
1480
  }
1037
1481
  clearAllOtaTimers() {