@glassly/engine 0.3.7 → 0.3.8-beta.1013

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 (229) hide show
  1. package/README.md +4 -0
  2. package/build/engine.d.ts +18 -2
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -4
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/ai.d.ts +3 -0
  7. package/build/facades/ai.d.ts.map +1 -1
  8. package/build/facades/ai.js +3 -0
  9. package/build/facades/ai.js.map +1 -1
  10. package/build/facades/backup.d.ts +14 -0
  11. package/build/facades/backup.d.ts.map +1 -0
  12. package/build/facades/backup.js +22 -0
  13. package/build/facades/backup.js.map +1 -0
  14. package/build/facades/glasses.d.ts +13 -5
  15. package/build/facades/glasses.d.ts.map +1 -1
  16. package/build/facades/glasses.js +17 -4
  17. package/build/facades/glasses.js.map +1 -1
  18. package/build/generated/releaseMetadata.js +4 -4
  19. package/build/generated/releaseMetadata.js.map +1 -1
  20. package/build/index.d.ts +3 -1
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js.map +1 -1
  23. package/build/services/AppRegistry.d.ts +2 -1
  24. package/build/services/AppRegistry.d.ts.map +1 -1
  25. package/build/services/AppRegistry.js +38 -8
  26. package/build/services/AppRegistry.js.map +1 -1
  27. package/build/services/AppStateBackup.d.ts +98 -0
  28. package/build/services/AppStateBackup.d.ts.map +1 -0
  29. package/build/services/AppStateBackup.js +224 -0
  30. package/build/services/AppStateBackup.js.map +1 -0
  31. package/build/services/BlobStore.d.ts.map +1 -1
  32. package/build/services/BlobStore.js +5 -12
  33. package/build/services/BlobStore.js.map +1 -1
  34. package/build/services/CloudClientService.d.ts +3 -1
  35. package/build/services/CloudClientService.d.ts.map +1 -1
  36. package/build/services/CloudClientService.js +42 -2
  37. package/build/services/CloudClientService.js.map +1 -1
  38. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  39. package/build/services/DeviceEventRouter.js +1 -0
  40. package/build/services/DeviceEventRouter.js.map +1 -1
  41. package/build/services/DisplayDiagnostics.d.ts.map +1 -1
  42. package/build/services/DisplayDiagnostics.js +4 -2
  43. package/build/services/DisplayDiagnostics.js.map +1 -1
  44. package/build/services/DisplayProcessor.d.ts +2 -0
  45. package/build/services/DisplayProcessor.d.ts.map +1 -1
  46. package/build/services/DisplayProcessor.js +15 -1
  47. package/build/services/DisplayProcessor.js.map +1 -1
  48. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  49. package/build/services/GlassesTouchInput.js +4 -2
  50. package/build/services/GlassesTouchInput.js.map +1 -1
  51. package/build/services/JSCrashloopReportService.d.ts.map +1 -1
  52. package/build/services/JSCrashloopReportService.js +5 -0
  53. package/build/services/JSCrashloopReportService.js.map +1 -1
  54. package/build/services/JSRouter.d.ts +11 -3
  55. package/build/services/JSRouter.d.ts.map +1 -1
  56. package/build/services/JSRouter.js +36 -6
  57. package/build/services/JSRouter.js.map +1 -1
  58. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  59. package/build/services/LocalDisplayManager.js +5 -2
  60. package/build/services/LocalDisplayManager.js.map +1 -1
  61. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  62. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  63. package/build/services/LocalMiniappRuntime.js +61 -22
  64. package/build/services/LocalMiniappRuntime.js.map +1 -1
  65. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  66. package/build/services/LocalSttFallbackCoordinator.js +2 -0
  67. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  68. package/build/services/LocalTranslationService.d.ts +2 -0
  69. package/build/services/LocalTranslationService.d.ts.map +1 -1
  70. package/build/services/LocalTranslationService.js +22 -0
  71. package/build/services/LocalTranslationService.js.map +1 -1
  72. package/build/services/MiniappEngine.d.ts +9 -0
  73. package/build/services/MiniappEngine.d.ts.map +1 -1
  74. package/build/services/MiniappEngine.js +60 -27
  75. package/build/services/MiniappEngine.js.map +1 -1
  76. package/build/services/MiniappLauncher.d.ts +2 -0
  77. package/build/services/MiniappLauncher.d.ts.map +1 -1
  78. package/build/services/MiniappLauncher.js +6 -0
  79. package/build/services/MiniappLauncher.js.map +1 -1
  80. package/build/services/NavigationService.d.ts +1 -0
  81. package/build/services/NavigationService.d.ts.map +1 -1
  82. package/build/services/NavigationService.js +20 -6
  83. package/build/services/NavigationService.js.map +1 -1
  84. package/build/services/OfflineActivity.d.ts +54 -0
  85. package/build/services/OfflineActivity.d.ts.map +1 -0
  86. package/build/services/OfflineActivity.js +86 -0
  87. package/build/services/OfflineActivity.js.map +1 -0
  88. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  89. package/build/services/PhoneCalendarService.js +8 -0
  90. package/build/services/PhoneCalendarService.js.map +1 -1
  91. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  92. package/build/services/PhoneNotificationsSync.js +7 -3
  93. package/build/services/PhoneNotificationsSync.js.map +1 -1
  94. package/build/services/STTModelManager.d.ts +1 -0
  95. package/build/services/STTModelManager.d.ts.map +1 -1
  96. package/build/services/STTModelManager.js +6 -0
  97. package/build/services/STTModelManager.js.map +1 -1
  98. package/build/services/SceneRenderer.js +1 -1
  99. package/build/services/SceneRenderer.js.map +1 -1
  100. package/build/services/appStateBackupFile.d.ts +32 -0
  101. package/build/services/appStateBackupFile.d.ts.map +1 -0
  102. package/build/services/appStateBackupFile.js +135 -0
  103. package/build/services/appStateBackupFile.js.map +1 -0
  104. package/build/services/controllers.d.ts.map +1 -1
  105. package/build/services/controllers.js +13 -3
  106. package/build/services/controllers.js.map +1 -1
  107. package/build/services/effectiveCapabilities.js +6 -1
  108. package/build/services/effectiveCapabilities.js.map +1 -1
  109. package/build/services/neuralBand/NeuralBandInput.d.ts +12 -1
  110. package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -1
  111. package/build/services/neuralBand/NeuralBandInput.js +27 -0
  112. package/build/services/neuralBand/NeuralBandInput.js.map +1 -1
  113. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -1
  114. package/build/services/neuralBand/NeuralBandSettingsSync.js +6 -0
  115. package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -1
  116. package/build/services/pages/pageEligibility.d.ts.map +1 -1
  117. package/build/services/pages/pageEligibility.js +8 -1
  118. package/build/services/pages/pageEligibility.js.map +1 -1
  119. package/build/services/settingsBridge.d.ts.map +1 -1
  120. package/build/services/settingsBridge.js +1 -0
  121. package/build/services/settingsBridge.js.map +1 -1
  122. package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
  123. package/build/services/watch/WatchLink.d.ts.map +1 -0
  124. package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
  125. package/build/services/watch/WatchLink.js.map +1 -0
  126. package/build/stores/appVisibility.d.ts.map +1 -1
  127. package/build/stores/appVisibility.js +0 -1
  128. package/build/stores/appVisibility.js.map +1 -1
  129. package/build/stores/apps.d.ts.map +1 -1
  130. package/build/stores/apps.js +22 -10
  131. package/build/stores/apps.js.map +1 -1
  132. package/build/stores/settings.d.ts.map +1 -1
  133. package/build/stores/settings.js +71 -6
  134. package/build/stores/settings.js.map +1 -1
  135. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  136. package/build/types/capabilities/even-realities-g1.js +3 -0
  137. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  138. package/build/types/capabilities/memomind-one.d.ts +3 -0
  139. package/build/types/capabilities/memomind-one.d.ts.map +1 -0
  140. package/build/types/capabilities/memomind-one.js +42 -0
  141. package/build/types/capabilities/memomind-one.js.map +1 -0
  142. package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
  143. package/build/types/capabilities/rayneo-io.js +3 -2
  144. package/build/types/capabilities/rayneo-io.js.map +1 -1
  145. package/build/types/enums.d.ts +8 -2
  146. package/build/types/enums.d.ts.map +1 -1
  147. package/build/types/enums.js +6 -0
  148. package/build/types/enums.js.map +1 -1
  149. package/build/types/hardware.d.ts +3 -1
  150. package/build/types/hardware.d.ts.map +1 -1
  151. package/build/types/hardware.js +3 -1
  152. package/build/types/hardware.js.map +1 -1
  153. package/build/utils/display/index.d.ts +2 -1
  154. package/build/utils/display/index.d.ts.map +1 -1
  155. package/build/utils/display/index.js +3 -2
  156. package/build/utils/display/index.js.map +1 -1
  157. package/build/utils/display/profiles/index.d.ts +2 -1
  158. package/build/utils/display/profiles/index.d.ts.map +1 -1
  159. package/build/utils/display/profiles/index.js +3 -2
  160. package/build/utils/display/profiles/index.js.map +1 -1
  161. package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
  162. package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
  163. package/build/utils/display/profiles/rayneo-io.js +1 -1
  164. package/build/utils/display/profiles/rayneo-io.js.map +1 -1
  165. package/build/utils/display/scene/index.d.ts +1 -1
  166. package/build/utils/display/scene/index.d.ts.map +1 -1
  167. package/build/utils/display/scene/index.js +1 -1
  168. package/build/utils/display/scene/index.js.map +1 -1
  169. package/build/utils/display/scene/process.d.ts +6 -0
  170. package/build/utils/display/scene/process.d.ts.map +1 -1
  171. package/build/utils/display/scene/process.js +22 -7
  172. package/build/utils/display/scene/process.js.map +1 -1
  173. package/build/utils/miniappGlobals.d.ts +13 -17
  174. package/build/utils/miniappGlobals.d.ts.map +1 -1
  175. package/build/utils/miniappGlobals.js +24 -56
  176. package/build/utils/miniappGlobals.js.map +1 -1
  177. package/package.json +7 -7
  178. package/src/engine.ts +7 -4
  179. package/src/facades/ai.ts +4 -0
  180. package/src/facades/backup.ts +28 -0
  181. package/src/facades/glasses.ts +22 -5
  182. package/src/generated/releaseMetadata.ts +4 -4
  183. package/src/index.ts +10 -0
  184. package/src/services/AppRegistry.ts +36 -9
  185. package/src/services/AppStateBackup.ts +283 -0
  186. package/src/services/BlobStore.ts +5 -11
  187. package/src/services/CloudClientService.ts +43 -4
  188. package/src/services/DeviceEventRouter.ts +1 -0
  189. package/src/services/DisplayDiagnostics.ts +4 -2
  190. package/src/services/DisplayProcessor.ts +20 -8
  191. package/src/services/GlassesTouchInput.ts +6 -2
  192. package/src/services/JSCrashloopReportService.ts +5 -0
  193. package/src/services/JSRouter.ts +37 -6
  194. package/src/services/LocalDisplayManager.ts +5 -2
  195. package/src/services/LocalMiniappRuntime.ts +61 -22
  196. package/src/services/LocalSttFallbackCoordinator.ts +4 -0
  197. package/src/services/LocalTranslationService.ts +20 -0
  198. package/src/services/MiniappEngine.ts +62 -27
  199. package/src/services/MiniappLauncher.ts +7 -0
  200. package/src/services/NavigationService.ts +28 -8
  201. package/src/services/OfflineActivity.ts +131 -0
  202. package/src/services/PhoneCalendarService.ts +7 -0
  203. package/src/services/PhoneNotificationsSync.ts +8 -3
  204. package/src/services/STTModelManager.ts +6 -0
  205. package/src/services/SceneRenderer.ts +1 -1
  206. package/src/services/appStateBackupFile.ts +171 -0
  207. package/src/services/controllers.ts +14 -3
  208. package/src/services/effectiveCapabilities.ts +6 -1
  209. package/src/services/neuralBand/NeuralBandInput.ts +37 -1
  210. package/src/services/neuralBand/NeuralBandSettingsSync.ts +12 -0
  211. package/src/services/pages/pageEligibility.ts +9 -1
  212. package/src/services/settingsBridge.ts +1 -0
  213. package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
  214. package/src/stores/appVisibility.ts +0 -1
  215. package/src/stores/apps.ts +22 -11
  216. package/src/stores/settings.ts +78 -10
  217. package/src/types/capabilities/even-realities-g1.ts +4 -0
  218. package/src/types/capabilities/memomind-one.ts +43 -0
  219. package/src/types/capabilities/rayneo-io.ts +3 -2
  220. package/src/types/enums.ts +6 -0
  221. package/src/types/hardware.ts +14 -1
  222. package/src/utils/display/index.ts +4 -2
  223. package/src/utils/display/profiles/index.ts +9 -2
  224. package/src/utils/display/profiles/rayneo-io.ts +6 -1
  225. package/src/utils/display/scene/index.ts +1 -1
  226. package/src/utils/display/scene/process.ts +22 -7
  227. package/src/utils/miniappGlobals.ts +32 -64
  228. package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
  229. package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
package/src/index.ts CHANGED
@@ -64,11 +64,20 @@ export type {
64
64
  LocalLlmModelId,
65
65
  } from "./services/LocalLlmModelManager"
66
66
  export {DEFAULT_LOCAL_LLM_MODEL, isLocalLlmModelId} from "./services/LocalLlmModelManager"
67
+ export type {
68
+ OfflineActivity,
69
+ OfflineActivityReason,
70
+ OfflineLlmActivity,
71
+ OfflineTranscriptionActivity,
72
+ OfflineTranslationActivity,
73
+ OfflineTranslationPair,
74
+ } from "./services/OfflineActivity"
67
75
 
68
76
  // Shared user memory timeline (engine.memory / Settings > Memory). Entry and
69
77
  // session shapes are canonical in @glassly/miniapp (the session.memory
70
78
  // surface); the stats read model is engine-side.
71
79
  export type {MemoryStats} from "./services/MemoryStore"
80
+ export type {AppStateExportResult, AppStateRestoreOptions, AppStateRestoreResult} from "./services/appStateBackupFile"
72
81
  export type {SpeakerProfile} from "./services/SpeakerProfileStore"
73
82
  export type {EnrollOutcome} from "./facades/speakers"
74
83
  export {MENTRA_LIVE_SETTING_KEYS, getBluetoothSettingKeysForDevice} from "./stores/bluetoothSettingKeys"
@@ -230,6 +239,7 @@ export {
230
239
  } from "./services/neuralBand/MetaAuth"
231
240
  export type {
232
241
  NeuralBandClaimStatus,
242
+ NeuralBandFirmwareStatus,
233
243
  NeuralBandHandwritingStatus,
234
244
  NeuralBandHandwritingUpdate,
235
245
  } from "./services/neuralBand/NeuralBandInput"
@@ -658,7 +658,8 @@ class AppRegistry {
658
658
  }
659
659
  }
660
660
 
661
- public async getActiveVersion(packageName: string): Promise<string> {
661
+ /** Undefined when no bundle is on disk (offline built-ins, HTTP-direct dev apps). */
662
+ public async getActiveVersion(packageName: string): Promise<string | undefined> {
662
663
  let versions = this.getInstalledVersions(packageName)
663
664
  // Treat MMKV as a hint, not authority. A stored version may have been
664
665
  // GC'd off disk without this pointer being updated.
@@ -677,8 +678,10 @@ class AppRegistry {
677
678
  }
678
679
  versions = versions.filter((v) => semver.valid(v))
679
680
  versions.sort((a, b) => semver.rcompare(a, b))
680
- this.setActiveVersion(packageName, versions[0])
681
- return versions[0]
681
+ const latest = versions[0] as string | undefined
682
+ // Never persist undefined: MMKV.set throws on it.
683
+ if (latest) this.setActiveVersion(packageName, latest)
684
+ return latest
682
685
  }
683
686
 
684
687
  public setActiveVersion(packageName: string, version: string): Result<void, Error> {
@@ -727,7 +730,13 @@ class AppRegistry {
727
730
  const installed = diskApps.filter(
728
731
  (app) => !offlinePackages.has(app.packageName) && !devPackages.has(app.packageName),
729
732
  )
730
- return [...installed, ...dev, ...offline]
733
+ // The last switcher-card capture comes back from disk, so a relaunched
734
+ // app's card is not blank until it is minimized again.
735
+ const withScreenshot = (app: ClientApp): ClientApp => ({
736
+ ...app,
737
+ screenshot: app.screenshot ?? getLocalAppScreenshot(app.packageName),
738
+ })
739
+ return [...installed.map(withScreenshot), ...dev.map(withScreenshot), ...offline]
731
740
  }
732
741
 
733
742
  public async getInstalledMiniapps(): Promise<ClientApp[]> {
@@ -747,6 +756,8 @@ class AppRegistry {
747
756
  const out: ClientApp[] = []
748
757
  for (const lmaInfo of installedInfo) {
749
758
  const versionString = await this.getActiveVersion(lmaInfo.packageName)
759
+ // A package dir with no usable bundle (e.g. only a half-written version) has nothing to run.
760
+ if (!versionString) continue
750
761
  const versionInfo = lmaInfo.versions[versionString]
751
762
 
752
763
  const manifest = this.getMiniappManifest(lmaInfo.packageName, versionString) as {
@@ -963,6 +974,19 @@ export function saveWidgetExpandChoice(packageName: string, widgetId: string, ex
963
974
  storage.save(`${packageName}_widget_expand_${widgetId}`, expandOnTap)
964
975
  }
965
976
 
977
+ /**
978
+ * Where a screenshot captured under an earlier install now lives: iOS gives
979
+ * every install a new container, so a stored absolute file URI goes stale
980
+ * while the file itself is still in the documents directory.
981
+ */
982
+ function rerootDocumentUri(uri: string): string | null {
983
+ const marker = "/Documents/"
984
+ const at = uri.indexOf(marker)
985
+ if (at < 0) return null
986
+ const rerooted = new File(Paths.document, uri.slice(at + marker.length)).uri
987
+ return rerooted === uri ? null : rerooted
988
+ }
989
+
966
990
  export function getLocalAppScreenshot(packageName: string): string | undefined {
967
991
  const key = `${packageName}_screenshot`
968
992
  const res = storage.load<string>(key)
@@ -971,14 +995,17 @@ export function getLocalAppScreenshot(packageName: string): string | undefined {
971
995
  const uri = res.value
972
996
  if (uri.startsWith("file://")) {
973
997
  try {
974
- if (!new File(uri).exists) {
975
- storage.remove(key)
976
- return undefined
998
+ if (new File(uri).exists) return uri
999
+ const rerooted = rerootDocumentUri(uri)
1000
+ if (rerooted && new File(rerooted).exists) {
1001
+ storage.save(key, rerooted)
1002
+ return rerooted
977
1003
  }
978
1004
  } catch {
979
- storage.remove(key)
980
- return undefined
1005
+ // fall through: the stored uri is unusable
981
1006
  }
1007
+ storage.remove(key)
1008
+ return undefined
982
1009
  }
983
1010
 
984
1011
  return uri
@@ -0,0 +1,283 @@
1
+ /**
2
+ * App state backups (Settings > Profile > Backup): one JSON file carrying the
3
+ * system settings, every miniapp's SimpleStorage, the installed miniapp list
4
+ * with each app's home flags, and the memory timeline.
5
+ *
6
+ * It exists so a logout or reinstall doesn't mean signing back into every
7
+ * miniapp, which is why the file holds live credentials (miniapp tokens,
8
+ * provider API keys) as plain JSON. Pairing identity, cloud endpoints, and
9
+ * consent/login state stay out: they belong to the install and the account,
10
+ * not to the user's preferences.
11
+ *
12
+ * Storage keys are written without their user segment, so a backup restores
13
+ * into whatever namespace the phone has now (account or `local_`). Restore
14
+ * overwrites key by key and leaves keys the backup doesn't carry alone.
15
+ *
16
+ * Pure: the MMKV view, settings registry, and memory snapshot are passed in by
17
+ * appStateBackupFile.ts.
18
+ */
19
+
20
+ import type {MemoryBackup} from "@glassly/miniapp"
21
+
22
+ /** Key-value surface (MMKV in production, values already JSON-decoded). */
23
+ export interface AppStateBackupBackend {
24
+ get(key: string): unknown | null
25
+ set(key: string, value: unknown): void
26
+ keys(): string[]
27
+ }
28
+
29
+ /** The fields of a SETTINGS descriptor that decide whether it is backed up. */
30
+ export interface BackupSettingDescriptor {
31
+ key: string
32
+ persist: boolean
33
+ writable: boolean
34
+ nativeAuthoritative?: true
35
+ resetOnBuildEnvChange?: true
36
+ indexer?: (key: string) => string
37
+ }
38
+
39
+ export interface AppStateBackupMiniapp {
40
+ /** Set when the miniapp was installed at export. */
41
+ installed?: {version: string; source?: string}
42
+ /** SimpleStorage key → stored value. */
43
+ storage: Record<string, unknown>
44
+ /** Home flags by suffix: `hidden`, `always_on`, `widget_expand_<widgetId>`. */
45
+ prefs: Record<string, unknown>
46
+ }
47
+
48
+ export interface AppStateBackup {
49
+ format: "glassly-app-state"
50
+ version: 1
51
+ /** Epoch ms the backup was written. */
52
+ exportedAt: number
53
+ /** Stored setting values; indexed settings keep their `key:index` storage key. */
54
+ settings: Record<string, unknown>
55
+ miniapps: Record<string, AppStateBackupMiniapp>
56
+ memory: MemoryBackup | null
57
+ }
58
+
59
+ /** Thrown for a file that isn't an app state backup this build can read. */
60
+ export class AppStateBackupError extends Error {
61
+ constructor(message: string) {
62
+ super(message)
63
+ this.name = "AppStateBackupError"
64
+ }
65
+ }
66
+
67
+ const STORAGE_ROOT = "localstorage_"
68
+ const PREF_SUFFIXES = ["hidden", "always_on"] as const
69
+ const WIDGET_EXPAND_PREFIX = "widget_expand_"
70
+ // Same rule as the miniapp.json schema; keeps a hand-edited file from writing keys no app reads.
71
+ const PACKAGE_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/
72
+
73
+ /**
74
+ * Persisted, writable settings that are still not the user's to carry between
75
+ * installs: account and consent state, the paired devices, and caches.
76
+ */
77
+ const EXCLUDED_SETTINGS = new Set([
78
+ "auth_email",
79
+ "disable_cloud",
80
+ "ai_data_consent_version",
81
+ "ai_data_consent_agreed",
82
+ "login_skipped",
83
+ "has_paired_glasses",
84
+ "keyboard_name",
85
+ "cached_required_version",
86
+ "offline_camera_running",
87
+ "offline_translation_running",
88
+ ])
89
+
90
+ export function isBackupSetting(setting: BackupSettingDescriptor): boolean {
91
+ return (
92
+ setting.persist &&
93
+ setting.writable &&
94
+ !setting.nativeAuthoritative &&
95
+ !setting.resetOnBuildEnvChange &&
96
+ !EXCLUDED_SETTINGS.has(setting.key)
97
+ )
98
+ }
99
+
100
+ export interface CollectAppStateInput {
101
+ backend: AppStateBackupBackend
102
+ userId: string
103
+ settings: BackupSettingDescriptor[]
104
+ installed: Array<{packageName: string; version: string; source?: string}>
105
+ memory: MemoryBackup | null
106
+ now: number
107
+ }
108
+
109
+ export function collectAppStateBackup(input: CollectAppStateInput): AppStateBackup {
110
+ const {backend, settings} = input
111
+ const keys = backend.keys()
112
+
113
+ const settingValues: Record<string, unknown> = {}
114
+ for (const setting of settings) {
115
+ if (!isBackupSetting(setting)) continue
116
+ if (setting.indexer) {
117
+ for (const key of keys) {
118
+ if (!key.startsWith(`${setting.key}:`)) continue
119
+ const value = backend.get(key)
120
+ if (value != null) settingValues[key] = value
121
+ }
122
+ continue
123
+ }
124
+ const value = backend.get(setting.key)
125
+ if (value != null) settingValues[setting.key] = value
126
+ }
127
+
128
+ const miniapps: Record<string, AppStateBackupMiniapp> = {}
129
+ const entry = (packageName: string) => (miniapps[packageName] ??= {storage: {}, prefs: {}})
130
+
131
+ for (const app of input.installed) {
132
+ const record = entry(app.packageName)
133
+ record.installed = app.source ? {version: app.version, source: app.source} : {version: app.version}
134
+ for (const suffix of PREF_SUFFIXES) {
135
+ const value = backend.get(`${app.packageName}_${suffix}`)
136
+ if (value != null) record.prefs[suffix] = value
137
+ }
138
+ const expandPrefix = `${app.packageName}_${WIDGET_EXPAND_PREFIX}`
139
+ for (const key of keys) {
140
+ if (!key.startsWith(expandPrefix)) continue
141
+ const value = backend.get(key)
142
+ if (value != null) record.prefs[key.slice(app.packageName.length + 1)] = value
143
+ }
144
+ }
145
+
146
+ // Longest first, so a package whose name extends another's still claims its own keys.
147
+ const known = input.installed.map((app) => app.packageName).sort((a, b) => b.length - a.length)
148
+ const prefix = `${STORAGE_ROOT}${input.userId}_`
149
+ for (const key of keys) {
150
+ if (!key.startsWith(prefix)) continue
151
+ const split = splitStorageKey(key.slice(prefix.length), known)
152
+ if (!split) continue
153
+ const value = backend.get(key)
154
+ if (value != null) entry(split.packageName).storage[split.key] = value
155
+ }
156
+
157
+ return {
158
+ format: "glassly-app-state",
159
+ version: 1,
160
+ exportedAt: input.now,
161
+ settings: settingValues,
162
+ miniapps,
163
+ memory: input.memory,
164
+ }
165
+ }
166
+
167
+ /** `{packageName}_{key}` → its parts. Package names may contain `_`, so known names win over the first `_`. */
168
+ function splitStorageKey(rest: string, known: string[]): {packageName: string; key: string} | null {
169
+ const packageName = known.find((name) => rest.startsWith(`${name}_`)) ?? rest.slice(0, rest.indexOf("_"))
170
+ const key = rest.slice(packageName.length + 1)
171
+ if (!packageName || !key) return null
172
+ return {packageName, key}
173
+ }
174
+
175
+ export function parseAppStateBackup(data: unknown): AppStateBackup {
176
+ if (!isRecord(data) || data.format !== "glassly-app-state") {
177
+ throw new AppStateBackupError("Not a Glassly app state backup")
178
+ }
179
+ if (typeof data.version !== "number" || data.version > 1) {
180
+ throw new AppStateBackupError(`Unsupported app state backup version ${String(data.version)}`)
181
+ }
182
+
183
+ const settings = isRecord(data.settings) ? data.settings : {}
184
+ const miniapps: Record<string, AppStateBackupMiniapp> = {}
185
+ if (isRecord(data.miniapps)) {
186
+ for (const [packageName, raw] of Object.entries(data.miniapps)) {
187
+ if (!PACKAGE_NAME_RE.test(packageName) || !isRecord(raw)) continue
188
+ const record: AppStateBackupMiniapp = {
189
+ storage: isRecord(raw.storage) ? raw.storage : {},
190
+ prefs: isRecord(raw.prefs) ? raw.prefs : {},
191
+ }
192
+ if (isRecord(raw.installed) && typeof raw.installed.version === "string") {
193
+ record.installed = {version: raw.installed.version}
194
+ if (typeof raw.installed.source === "string") record.installed.source = raw.installed.source
195
+ }
196
+ miniapps[packageName] = record
197
+ }
198
+ }
199
+
200
+ let memory: MemoryBackup | null = null
201
+ if (data.memory != null) {
202
+ if (!isRecord(data.memory) || data.memory.format !== "glassly-memory") {
203
+ throw new AppStateBackupError("The backup's memory section is not a memory backup")
204
+ }
205
+ memory = data.memory as unknown as MemoryBackup
206
+ }
207
+
208
+ return {format: "glassly-app-state", version: 1, exportedAt: Number(data.exportedAt) || 0, settings, miniapps, memory}
209
+ }
210
+
211
+ /**
212
+ * The backup's settings this build will restore, split by how they're written:
213
+ * `plain` through the settings store, `indexed` (`key:index`) straight to storage.
214
+ * Keys this build doesn't know, or doesn't back up, are dropped.
215
+ */
216
+ export function restorableSettings(
217
+ backup: AppStateBackup,
218
+ settings: BackupSettingDescriptor[],
219
+ ): {plain: Record<string, unknown>; indexed: Record<string, unknown>} {
220
+ const byKey = new Map(settings.map((setting) => [setting.key, setting]))
221
+ const plain: Record<string, unknown> = {}
222
+ const indexed: Record<string, unknown> = {}
223
+ for (const [key, value] of Object.entries(backup.settings)) {
224
+ if (value == null) continue
225
+ const colon = key.indexOf(":")
226
+ const setting = byKey.get(colon < 0 ? key : key.slice(0, colon))
227
+ if (!setting || !isBackupSetting(setting)) continue
228
+ if (colon < 0 && !setting.indexer) plain[key] = value
229
+ else if (colon > 0 && setting.indexer) indexed[key] = value
230
+ }
231
+ return {plain, indexed}
232
+ }
233
+
234
+ /** Miniapps the backup has storage for. */
235
+ export function miniappsWithStorage(backup: AppStateBackup): string[] {
236
+ return Object.entries(backup.miniapps)
237
+ .filter(([, record]) => Object.entries(record.storage).some(([key, value]) => key && value != null))
238
+ .map(([packageName]) => packageName)
239
+ }
240
+
241
+ /** Write every miniapp's storage into `userId`'s namespace. Returns the packages that got keys. */
242
+ export function restoreMiniappStorage(
243
+ backend: AppStateBackupBackend,
244
+ userId: string,
245
+ backup: AppStateBackup,
246
+ ): {packages: string[]; keys: number} {
247
+ const packages: string[] = []
248
+ let keys = 0
249
+ for (const [packageName, record] of Object.entries(backup.miniapps)) {
250
+ let wrote = 0
251
+ for (const [key, value] of Object.entries(record.storage)) {
252
+ if (!key || value == null) continue
253
+ backend.set(`${STORAGE_ROOT}${userId}_${packageName}_${key}`, value)
254
+ wrote++
255
+ }
256
+ if (wrote > 0) packages.push(packageName)
257
+ keys += wrote
258
+ }
259
+ return {packages, keys}
260
+ }
261
+
262
+ export interface MiniappPrefs {
263
+ hidden?: boolean
264
+ alwaysOn?: boolean
265
+ widgetExpand: Record<string, boolean>
266
+ }
267
+
268
+ /** A miniapp's home flags from the backup, with anything that isn't a boolean dropped. */
269
+ export function miniappPrefs(record: AppStateBackupMiniapp): MiniappPrefs {
270
+ const prefs: MiniappPrefs = {widgetExpand: {}}
271
+ if (typeof record.prefs.hidden === "boolean") prefs.hidden = record.prefs.hidden
272
+ if (typeof record.prefs.always_on === "boolean") prefs.alwaysOn = record.prefs.always_on
273
+ for (const [key, value] of Object.entries(record.prefs)) {
274
+ if (!key.startsWith(WIDGET_EXPAND_PREFIX) || typeof value !== "boolean") continue
275
+ const widgetId = key.slice(WIDGET_EXPAND_PREFIX.length)
276
+ if (widgetId) prefs.widgetExpand[widgetId] = value
277
+ }
278
+ return prefs
279
+ }
280
+
281
+ function isRecord(value: unknown): value is Record<string, unknown> {
282
+ return value != null && typeof value === "object" && !Array.isArray(value)
283
+ }
@@ -504,20 +504,14 @@ export class BlobStore {
504
504
  const key = (typeof payload.key === "string" && payload.key) || this.makeId()
505
505
  const mimeFilter = payload.mimeType as string | undefined
506
506
 
507
- let picked: File | File[] | null = null
508
- try {
509
- picked = await File.pickFileAsync(undefined, mimeFilter)
510
- } catch (err) {
511
- // Picker dismissed / cancelled (or unavailable) → resolve to null, not an error.
512
- console.log(`${LOG_TAG}: blob import picker dismissed`, err)
513
- this.hooks.sendResult(packageName, requestId, true, null)
514
- return
515
- }
516
- const file = Array.isArray(picked) ? picked[0] : picked
517
- if (!file) {
507
+ // The options form resolves {canceled, result} and never throws; the deprecated
508
+ // (initialUri, mimeType) form resolves a bare File only when a mime type is passed.
509
+ const picked = await File.pickFileAsync(mimeFilter ? {mimeTypes: mimeFilter} : {})
510
+ if (picked.canceled) {
518
511
  this.hooks.sendResult(packageName, requestId, true, null)
519
512
  return
520
513
  }
514
+ const file = picked.result
521
515
  try {
522
516
  const size = file.size ?? 0
523
517
  if (this.overQuota(packageName, key, size)) {
@@ -19,9 +19,10 @@ import type {
19
19
  RuntimeSnapshot,
20
20
  StoreCatalogResponse,
21
21
  StoreListing,
22
+ StoreReleasesResponse,
22
23
  } from "@glassly/cloud-client/react-native"
23
24
  import type {SubjectTokenType} from "@glassly/cloud-client"
24
- import {Platform} from "react-native"
25
+ import {AppState, Platform} from "react-native"
25
26
  import type {AudioSubscription, QuotaStatusPayload, TranscriptionData, TranslationData} from "@glassly/cloud-protocol"
26
27
 
27
28
  import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
@@ -70,6 +71,8 @@ const LOCAL_AUTH_PORT = 3002
70
71
 
71
72
  let client: CloudClient | null = null
72
73
  let connected = false
74
+ /** The cloud closed us for a newer socket; the client stays down until reconnect(). */
75
+ let superseded = false
73
76
  let persistentFailureTimer: ReturnType<typeof BgTimer.setTimeout> | null = null
74
77
  let persistentFailureNotified = false
75
78
  let audioSubscriptions: AudioSubscription[] = []
@@ -84,6 +87,7 @@ let authStateUnsubscribe: (() => void) | null = null
84
87
  let localDevRuntimeToken: {runtimeUrl: string; token: string; expiresAtMs: number} | null = null
85
88
  let lc3FrameSizeUnsubscribe: (() => void) | null = null
86
89
  let disableCloudUnsubscribe: (() => void) | null = null
90
+ let foregroundUnsubscribe: (() => void) | null = null
87
91
  let coreTokenSyncPromise: Promise<string> | null = null
88
92
  /**
89
93
  * A device-local namespace (`local_…`) whose data still needs to be adopted
@@ -511,6 +515,26 @@ function stopLc3FrameSizeWatcher(): void {
511
515
  lc3FrameSizeUnsubscribe = null
512
516
  }
513
517
 
518
+ /**
519
+ * Reclaim a superseded session only on foreground: the user picked this phone
520
+ * up, and a reclaim on any other signal would fight the newer socket.
521
+ */
522
+ function startForegroundWatcher(): void {
523
+ if (foregroundUnsubscribe) return
524
+
525
+ const subscription = AppState.addEventListener("change", (state) => {
526
+ if (state !== "active" || !superseded || !client) return
527
+ console.log(`${LOG_TAG}: foregrounded after supersede; reconnecting runtime`)
528
+ cloudClientService.reconnect()
529
+ })
530
+ foregroundUnsubscribe = () => subscription.remove()
531
+ }
532
+
533
+ function stopForegroundWatcher(): void {
534
+ foregroundUnsubscribe?.()
535
+ foregroundUnsubscribe = null
536
+ }
537
+
514
538
  /**
515
539
  * Apply a mid-session flip of the Privacy toggle without a restart: turning it
516
540
  * on tears the live client down, turning it back off reconnects. Installed by
@@ -596,6 +620,7 @@ function construct(): void {
596
620
  c.runtime.onConnected(() => {
597
621
  if (c !== client) return
598
622
  connected = true
623
+ superseded = false
599
624
  console.log(`${LOG_TAG}: runtime connected`)
600
625
  console.log(`${LOG_TAG}: debug: ws-session-debug connected`)
601
626
  // Cloud is up — cancel any pending persistent-failure alarm and re-arm for the
@@ -613,10 +638,9 @@ function construct(): void {
613
638
  c.runtime.onDisconnected((info) => {
614
639
  if (c !== client) return
615
640
  connected = false
641
+ superseded = /superseded by newer session/i.test(info.reason)
616
642
  console.log(`${LOG_TAG}: runtime disconnected (${info.reason})`)
617
- console.log(
618
- `${LOG_TAG}: debug: ws-session-debug disconnected reason=${info.reason} willStayDown=${/superseded by newer session/i.test(info.reason)}`,
619
- )
643
+ console.log(`${LOG_TAG}: debug: ws-session-debug disconnected reason=${info.reason} willStayDown=${superseded}`)
620
644
  // Arm the persistent-failure alarm once; if we're still down when it fires, raise
621
645
  // the notification. A reconnect within the window cancels it (onConnected above).
622
646
  if (!persistentFailureTimer && !persistentFailureNotified) {
@@ -659,6 +683,7 @@ function construct(): void {
659
683
  )
660
684
 
661
685
  startLc3FrameSizeWatcher()
686
+ startForegroundWatcher()
662
687
  }
663
688
 
664
689
  /**
@@ -720,6 +745,7 @@ export const cloudClientService = {
720
745
  client = null
721
746
  localDevRuntimeToken = null
722
747
  connected = false
748
+ superseded = false
723
749
  resetRuntimeStatus()
724
750
  if (wasConnected) notifyConnectionListeners(false)
725
751
 
@@ -753,6 +779,13 @@ export const cloudClientService = {
753
779
  return c.core.store.get(packageName)
754
780
  },
755
781
 
782
+ async getStoreReleases(packageName: string): Promise<StoreReleasesResponse> {
783
+ if (!client) this.init()
784
+ const c = client
785
+ if (!c?.core) throw new Error("cloud client core is unavailable")
786
+ return c.core.store.releases(packageName)
787
+ },
788
+
756
789
  /** Bytes of a store icon or screenshot; these routes need the user Bearer. */
757
790
  async fetchStoreAsset(url: string): Promise<Uint8Array> {
758
791
  if (!client) this.init()
@@ -835,11 +868,13 @@ export const cloudClientService = {
835
868
  console.warn(`${LOG_TAG}: stop close() failed: ${(err as Error)?.message ?? err}`)
836
869
  }
837
870
  stopLc3FrameSizeWatcher()
871
+ stopForegroundWatcher()
838
872
  stopAuthWatch()
839
873
  clearRuntimeEventSubscriptions()
840
874
  clearPersistentFailureAlarm()
841
875
  const wasConnected = connected
842
876
  client = null
877
+ superseded = false
843
878
  // stop() is part of the logout lifecycle and runs before the host emits
844
879
  // SIGNED_OUT. Remove the persisted namespace owner here so a later account
845
880
  // can never inherit it. A force-stop/process kill does not call stop(), so
@@ -855,6 +890,10 @@ export const cloudClientService = {
855
890
  shutdown(): void {
856
891
  this.stop()
857
892
  stopDisableCloudWatcher()
893
+ // The next engine.start() is configured afresh; an override left by a
894
+ // reconnect() in this process would otherwise outrank the new config
895
+ // and keep the client on the previous backend.
896
+ endpointsOverride = null
858
897
  },
859
898
 
860
899
  /** Device-side managed photo: presign now, deliver bytes, await ready. */
@@ -245,6 +245,7 @@ export function startDeviceEventRouter(): void {
245
245
  subs.push(BluetoothSdk.addListener("neural_band_claim", (event) => neuralBandInput.handleClaim(event)))
246
246
  subs.push(BluetoothSdk.addListener("neural_band_handwriting", (event) => neuralBandInput.handleHandwriting(event)))
247
247
  subs.push(BluetoothSdk.addListener("neural_band_pointer", (event) => neuralBandInput.handlePointer(event)))
248
+ subs.push(BluetoothSdk.addListener("neural_band_firmware", (event) => neuralBandInput.handleFirmware(event)))
248
249
  subs.push({
249
250
  remove: useGlassesStore.subscribe(
250
251
  (s) => s.keyboardConnected === true,
@@ -4,6 +4,7 @@ import type {DisplayDiagnostic} from "@glassly/miniapp"
4
4
  // messages, packet bytes, or arbitrary application content to another miniapp.
5
5
  const fields = new Set([
6
6
  "v",
7
+ "device",
7
8
  "run",
8
9
  "id",
9
10
  "t",
@@ -72,10 +73,11 @@ export function collectDisplayDiagnostics(
72
73
  const seen = new Set<string>()
73
74
  for (let i = entries.length - 1; i >= 0 && records.length < 180; i--) {
74
75
  const message = entries[i].message
75
- const offset = message.indexOf("G2PERF ")
76
+ const prefix = message.includes("MEMOMINDPERF ") ? "MEMOMINDPERF " : "G2PERF "
77
+ const offset = message.indexOf(prefix)
76
78
  if (offset < 0 || message.length > 8192) continue
77
79
  try {
78
- const raw = JSON.parse(message.slice(offset + 7))
80
+ const raw = JSON.parse(message.slice(offset + prefix.length))
79
81
  if (
80
82
  !raw ||
81
83
  raw.v !== 1 ||
@@ -20,6 +20,8 @@ import {
20
20
  Z100_PROFILE,
21
21
  NEX_PROFILE,
22
22
  RAYNEO_IO_PROFILE,
23
+ MEMOMIND_ONE_PROFILE,
24
+ RAYNEO_IO_CFW_PROFILE,
23
25
  TextMeasurer,
24
26
  TextWrapper,
25
27
  ColumnComposer,
@@ -42,12 +44,7 @@ import {isGlassesConnected} from "./GlassesReadiness"
42
44
  * Display event types that we process
43
45
  */
44
46
  export type DisplayLayoutType =
45
- | "text_wall"
46
- | "text_line"
47
- | "text_rows"
48
- | "reference_card"
49
- | "double_text_wall"
50
- | "bitmap_view"
47
+ "text_wall" | "text_line" | "text_rows" | "reference_card" | "double_text_wall" | "bitmap_view"
51
48
 
52
49
  /**
53
50
  * Raw display event from the cloud/WebSocket
@@ -208,6 +205,16 @@ const DEVICE_PROFILES: Partial<Record<DeviceTypes, DisplayProfile>> = {
208
205
  [DeviceTypes.NEX]: NEX_PROFILE,
209
206
  [DeviceTypes.MACH1]: Z100_PROFILE, // Mach1 uses same hardware as Vuzix Z100
210
207
  [DeviceTypes.RAYNEO_IO]: RAYNEO_IO_PROFILE,
208
+ [DeviceTypes.MEMOMIND_ONE]: MEMOMIND_ONE_PROFILE,
209
+ }
210
+
211
+ /**
212
+ * Scene text profiles for models whose Glassly CFW draws text with a font of
213
+ * its own, used while the CFW publishes a scene canvas.
214
+ */
215
+ const CFW_SCENE_PROFILES: Partial<Record<DeviceTypes, DisplayProfile>> = {
216
+ [DeviceTypes.RAYNEO_IO]: RAYNEO_IO_CFW_PROFILE,
217
+ [DeviceTypes.MEMOMIND_ONE]: MEMOMIND_ONE_PROFILE,
211
218
  }
212
219
 
213
220
  // =============================================================================
@@ -275,8 +282,7 @@ export class DisplayProcessor {
275
282
  // Seed the device profile from the saved default wearable, read off the engine
276
283
  // settings store directly (was a host settings hook).
277
284
  const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable) as
278
- | string
279
- | undefined
285
+ string | undefined
280
286
  if (defaultWearable) {
281
287
  this.setDeviceModel(defaultWearable)
282
288
  console.log(`DISPLAY_PROCESSOR: Initialized DisplayProcessor with default wearable: ${defaultWearable}`)
@@ -380,6 +386,12 @@ export class DisplayProcessor {
380
386
  return this.profile
381
387
  }
382
388
 
389
+ /** Profile scene text is wrapped with: the CFW's own while its scene page is up, else the model's. */
390
+ public getSceneProfile(): DisplayProfile {
391
+ const cfw = CFW_SCENE_PROFILES[this.deviceModel]
392
+ return cfw && (useGlassesStore.getState().sceneCanvasWidth ?? 0) > 0 ? cfw : this.profile
393
+ }
394
+
383
395
  // ===========================================================================
384
396
  // Display Event Processing
385
397
  // ===========================================================================
@@ -110,7 +110,7 @@ function routeTouchEvent(event: TouchEvent): void {
110
110
  const doubleTap = gesture === "double_tap"
111
111
  // The Neural Band's middle finger (index 1) shares the tap names. Either
112
112
  // finger leaves the app on screen; the firmware dashboard is the index finger's.
113
- const osDoubleTap = doubleTap && event.index !== 1
113
+ const osDoubleTap = doubleTap && !(event.source === 3 && event.index === 1)
114
114
  const osHeadUp = gesture === "head_up"
115
115
 
116
116
  if (doubleTap) {
@@ -156,7 +156,11 @@ function routeTouchEvent(event: TouchEvent): void {
156
156
  // pagesEligible: connected Glassly CFW glasses, whose firmware leaves the
157
157
  // dashboard to the phone; other firmware opens its own on a lift.
158
158
  const opensNativeDashboard = osDoubleTap || (osHeadUp && pagesEligible())
159
- if (opensNativeDashboard && !nativeNavigationService.isActive && isGlassesConnected(useGlassesStore.getState().connection)) {
159
+ if (
160
+ opensNativeDashboard &&
161
+ !nativeNavigationService.isActive &&
162
+ isGlassesConnected(useGlassesStore.getState().connection)
163
+ ) {
160
164
  useDisplayStore.getState().setDisplayEvent({layout: {layoutType: "unavailable", reason: "firmware-dashboard"}})
161
165
  void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
162
166
  console.error("DeviceEventRouter: showDashboard failed:", err)