@mentra/sdk 2.1.29-beta.2 → 2.1.29

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 (81) hide show
  1. package/dist/app/server/index.d.ts +70 -5
  2. package/dist/app/server/index.d.ts.map +1 -1
  3. package/dist/app/session/device-state.d.ts +83 -0
  4. package/dist/app/session/device-state.d.ts.map +1 -0
  5. package/dist/app/session/events.d.ts +9 -0
  6. package/dist/app/session/events.d.ts.map +1 -1
  7. package/dist/app/session/index.d.ts +23 -3
  8. package/dist/app/session/index.d.ts.map +1 -1
  9. package/dist/app/session/modules/camera.d.ts +5 -43
  10. package/dist/app/session/modules/camera.d.ts.map +1 -1
  11. package/dist/index.d.ts +6 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +389 -135
  14. package/dist/index.js.map +16 -14
  15. package/dist/types/capabilities.d.ts +3 -90
  16. package/dist/types/capabilities.d.ts.map +1 -1
  17. package/dist/types/index.d.ts +1 -1
  18. package/dist/types/index.d.ts.map +1 -1
  19. package/dist/types/message-types.d.ts +8 -3
  20. package/dist/types/message-types.d.ts.map +1 -1
  21. package/dist/types/messages/app-to-cloud.d.ts +15 -1
  22. package/dist/types/messages/app-to-cloud.d.ts.map +1 -1
  23. package/dist/types/messages/cloud-to-app.d.ts +14 -1
  24. package/dist/types/messages/cloud-to-app.d.ts.map +1 -1
  25. package/dist/types/messages/cloud-to-glasses.d.ts +10 -3
  26. package/dist/types/messages/cloud-to-glasses.d.ts.map +1 -1
  27. package/dist/types/messages/glasses-to-cloud.d.ts +20 -2
  28. package/dist/types/messages/glasses-to-cloud.d.ts.map +1 -1
  29. package/dist/utils/Observable.d.ts +92 -0
  30. package/dist/utils/Observable.d.ts.map +1 -0
  31. package/node_modules/@mentra/types/README.md +134 -0
  32. package/node_modules/@mentra/types/dist/applet.d.ts +39 -0
  33. package/node_modules/@mentra/types/dist/applet.d.ts.map +1 -0
  34. package/node_modules/@mentra/types/dist/applet.js +5 -0
  35. package/node_modules/@mentra/types/dist/capabilities/even-realities-g1.d.ts +12 -0
  36. package/node_modules/@mentra/types/dist/capabilities/even-realities-g1.d.ts.map +1 -0
  37. package/node_modules/@mentra/types/dist/capabilities/even-realities-g1.js +54 -0
  38. package/node_modules/@mentra/types/dist/capabilities/mentra-live.d.ts +12 -0
  39. package/node_modules/@mentra/types/dist/capabilities/mentra-live.d.ts.map +1 -0
  40. package/node_modules/@mentra/types/dist/capabilities/mentra-live.js +94 -0
  41. package/node_modules/@mentra/types/dist/capabilities/simulated-glasses.d.ts +13 -0
  42. package/node_modules/@mentra/types/dist/capabilities/simulated-glasses.d.ts.map +1 -0
  43. package/node_modules/@mentra/types/dist/capabilities/simulated-glasses.js +67 -0
  44. package/node_modules/@mentra/types/dist/capabilities/vuzix-z100.d.ts +12 -0
  45. package/node_modules/@mentra/types/dist/capabilities/vuzix-z100.d.ts.map +1 -0
  46. package/node_modules/@mentra/types/dist/capabilities/vuzix-z100.js +51 -0
  47. package/node_modules/@mentra/types/dist/cli.d.ts +130 -0
  48. package/node_modules/@mentra/types/dist/cli.d.ts.map +1 -0
  49. package/node_modules/@mentra/types/dist/cli.js +7 -0
  50. package/node_modules/@mentra/types/dist/device.d.ts +32 -0
  51. package/node_modules/@mentra/types/dist/device.d.ts.map +1 -0
  52. package/node_modules/@mentra/types/dist/device.js +6 -0
  53. package/node_modules/@mentra/types/dist/enums.d.ts +34 -0
  54. package/node_modules/@mentra/types/dist/enums.d.ts.map +1 -0
  55. package/node_modules/@mentra/types/dist/enums.js +39 -0
  56. package/node_modules/@mentra/types/dist/hardware.d.ts +141 -0
  57. package/node_modules/@mentra/types/dist/hardware.d.ts.map +1 -0
  58. package/node_modules/@mentra/types/dist/hardware.js +33 -0
  59. package/node_modules/@mentra/types/dist/index.d.ts +18 -0
  60. package/node_modules/@mentra/types/dist/index.d.ts.map +1 -0
  61. package/node_modules/@mentra/types/dist/index.js +25 -0
  62. package/node_modules/@mentra/types/package.json +31 -0
  63. package/node_modules/@mentra/types/src/applet.ts +51 -0
  64. package/node_modules/@mentra/types/src/capabilities/even-realities-g1.ts +63 -0
  65. package/node_modules/@mentra/types/src/capabilities/mentra-live.ts +103 -0
  66. package/node_modules/@mentra/types/src/capabilities/simulated-glasses.ts +76 -0
  67. package/node_modules/@mentra/types/src/capabilities/vuzix-z100.ts +60 -0
  68. package/node_modules/@mentra/types/src/cli.ts +169 -0
  69. package/node_modules/@mentra/types/src/device.ts +43 -0
  70. package/node_modules/@mentra/types/src/enums.ts +36 -0
  71. package/node_modules/@mentra/types/src/hardware.ts +172 -0
  72. package/node_modules/@mentra/types/src/index.ts +64 -0
  73. package/node_modules/@mentra/types/tsconfig.json +22 -0
  74. package/node_modules/@mentra/types/tsconfig.tsbuildinfo +1 -0
  75. package/package.json +6 -6
  76. package/dist/display-utils/test/ScrollView.test.d.ts +0 -2
  77. package/dist/display-utils/test/ScrollView.test.d.ts.map +0 -1
  78. package/dist/display-utils/test/TextMeasurer.test.d.ts +0 -2
  79. package/dist/display-utils/test/TextMeasurer.test.d.ts.map +0 -1
  80. package/dist/display-utils/test/TextWrapper.test.d.ts +0 -2
  81. package/dist/display-utils/test/TextWrapper.test.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -326,6 +326,8 @@ var init_message_types = __esm(() => {
326
326
  GlassesToCloudMessageType2["AUDIO_PLAY_RESPONSE"] = "audio_play_response";
327
327
  GlassesToCloudMessageType2["RGB_LED_CONTROL_RESPONSE"] = "rgb_led_control_response";
328
328
  GlassesToCloudMessageType2["LIVEKIT_INIT"] = "livekit_init";
329
+ GlassesToCloudMessageType2["UDP_REGISTER"] = "udp_register";
330
+ GlassesToCloudMessageType2["UDP_UNREGISTER"] = "udp_unregister";
329
331
  })(GlassesToCloudMessageType ||= {});
330
332
  ((CloudToGlassesMessageType2) => {
331
333
  CloudToGlassesMessageType2["CONNECTION_ACK"] = "connection_ack";
@@ -349,6 +351,7 @@ var init_message_types = __esm(() => {
349
351
  CloudToGlassesMessageType2["REQUEST_SINGLE_LOCATION"] = "request_single_location";
350
352
  CloudToGlassesMessageType2["WEBSOCKET_ERROR"] = "websocket_error";
351
353
  CloudToGlassesMessageType2["LIVEKIT_INFO"] = "livekit_info";
354
+ CloudToGlassesMessageType2["UDP_PING_ACK"] = "udp_ping_ack";
352
355
  })(CloudToGlassesMessageType ||= {});
353
356
  ((AppToCloudMessageType2) => {
354
357
  AppToCloudMessageType2["CONNECTION_INIT"] = "tpa_connection_init";
@@ -373,6 +376,7 @@ var init_message_types = __esm(() => {
373
376
  AppToCloudMessageType2["APP_USER_DISCOVERY"] = "app_user_discovery";
374
377
  AppToCloudMessageType2["APP_ROOM_JOIN"] = "app_room_join";
375
378
  AppToCloudMessageType2["APP_ROOM_LEAVE"] = "app_room_leave";
379
+ AppToCloudMessageType2["OWNERSHIP_RELEASE"] = "ownership_release";
376
380
  })(AppToCloudMessageType ||= {});
377
381
  ((CloudToAppMessageType2) => {
378
382
  CloudToAppMessageType2["CONNECTION_ACK"] = "tpa_connection_ack";
@@ -380,6 +384,7 @@ var init_message_types = __esm(() => {
380
384
  CloudToAppMessageType2["APP_STOPPED"] = "app_stopped";
381
385
  CloudToAppMessageType2["SETTINGS_UPDATE"] = "settings_update";
382
386
  CloudToAppMessageType2["CAPABILITIES_UPDATE"] = "capabilities_update";
387
+ CloudToAppMessageType2["DEVICE_STATE_UPDATE"] = "device_state_update";
383
388
  CloudToAppMessageType2["DASHBOARD_MODE_CHANGED"] = "dashboard_mode_changed";
384
389
  CloudToAppMessageType2["DASHBOARD_ALWAYS_ON_CHANGED"] = "dashboard_always_on_changed";
385
390
  CloudToAppMessageType2["DATA_STREAM"] = "data_stream";
@@ -735,6 +740,12 @@ function isAudioPlayResponse(message) {
735
740
  function isLocalTranscription(message) {
736
741
  return message.type === "local_transcription" /* LOCAL_TRANSCRIPTION */;
737
742
  }
743
+ function isUdpRegister(message) {
744
+ return message.type === "udp_register" /* UDP_REGISTER */;
745
+ }
746
+ function isUdpUnregister(message) {
747
+ return message.type === "udp_unregister" /* UDP_UNREGISTER */;
748
+ }
738
749
  // src/types/messages/cloud-to-glasses.ts
739
750
  init_message_types();
740
751
  function isResponse(message) {
@@ -829,6 +840,9 @@ function isRtmpStreamRequest(message) {
829
840
  function isRtmpStreamStopRequest(message) {
830
841
  return message.type === "rtmp_stream_stop" /* RTMP_STREAM_STOP */;
831
842
  }
843
+ function isOwnershipRelease(message) {
844
+ return message.type === "ownership_release" /* OWNERSHIP_RELEASE */;
845
+ }
832
846
  // src/utils/bitmap-utils.ts
833
847
  import * as fs from "fs/promises";
834
848
  import * as path from "path";
@@ -1339,6 +1353,9 @@ function isSettingsUpdate2(message) {
1339
1353
  function isCapabilitiesUpdate(message) {
1340
1354
  return message.type === "capabilities_update" /* CAPABILITIES_UPDATE */;
1341
1355
  }
1356
+ function isDeviceStateUpdate(message) {
1357
+ return message.type === "device_state_update" /* DEVICE_STATE_UPDATE */;
1358
+ }
1342
1359
  function isDataStream(message) {
1343
1360
  return message.type === "data_stream" /* DATA_STREAM */;
1344
1361
  }
@@ -1906,6 +1923,9 @@ class EventManager {
1906
1923
  this.unsubscribe(type);
1907
1924
  }
1908
1925
  }
1926
+ getRegisteredStreams() {
1927
+ return Array.from(this.handlers.keys());
1928
+ }
1909
1929
  emit(event, data) {
1910
1930
  try {
1911
1931
  this.emitter.emit(event, data);
@@ -2195,7 +2215,7 @@ class ApiClient {
2195
2215
  import pino from "pino";
2196
2216
  var BETTERSTACK_SOURCE_TOKEN = process.env.BETTERSTACK_SOURCE_TOKEN;
2197
2217
  var BETTERSTACK_ENDPOINT = process.env.BETTERSTACK_ENDPOINT || "https://s1311181.eu-nbg-2.betterstackdata.com";
2198
- var NODE_ENV = "isaiah";
2218
+ var NODE_ENV = "aryan";
2199
2219
  var PORTER_APP_NAME = process.env.PORTER_APP_NAME || "cloud-local";
2200
2220
  var LOG_LEVEL = NODE_ENV === "production" ? "info" : "debug";
2201
2221
  var streams2 = [];
@@ -2635,7 +2655,6 @@ class CameraModule {
2635
2655
  packageName;
2636
2656
  sessionId;
2637
2657
  logger;
2638
- pendingPhotoRequests = new Map;
2639
2658
  isStreaming = false;
2640
2659
  currentStreamUrl;
2641
2660
  currentStreamState;
@@ -2652,9 +2671,15 @@ class CameraModule {
2652
2671
  const baseUrl = this.session?.getHttpsServerUrl?.() || "";
2653
2672
  cameraWarnLog(baseUrl, this.packageName, "requestPhoto");
2654
2673
  try {
2655
- console.log("DEBUG: requestPhoto options:", options);
2656
2674
  const requestId = `photo_req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2657
- this.pendingPhotoRequests.set(requestId, { resolve, reject });
2675
+ this.session.appServer.registerPhotoRequest(requestId, {
2676
+ userId: this.session.userId,
2677
+ sessionId: this.sessionId,
2678
+ session: this.session,
2679
+ resolve,
2680
+ reject: (error) => reject(error.message),
2681
+ timestamp: Date.now()
2682
+ });
2658
2683
  const message = {
2659
2684
  type: "photo_request" /* PHOTO_REQUEST */,
2660
2685
  packageName: this.packageName,
@@ -2676,91 +2701,41 @@ class CameraModule {
2676
2701
  }, `\uD83D\uDCF8 Photo request sent`);
2677
2702
  if (options?.customWebhookUrl) {
2678
2703
  this.logger.info({ requestId, customWebhookUrl: options.customWebhookUrl }, `\uD83D\uDCF8 Using custom webhook URL - resolving promise immediately since photo will be uploaded directly to custom endpoint`);
2679
- const mockPhotoData = {
2680
- buffer: Buffer.from([]),
2681
- mimeType: "image/jpeg",
2682
- filename: "photo.jpg",
2683
- requestId,
2684
- size: 0,
2685
- timestamp: new Date
2686
- };
2687
- this.pendingPhotoRequests.delete(requestId);
2688
- resolve(mockPhotoData);
2704
+ const pending = this.session.appServer.completePhotoRequest(requestId);
2705
+ if (pending) {
2706
+ const mockPhotoData = {
2707
+ buffer: Buffer.from([]),
2708
+ mimeType: "image/jpeg",
2709
+ filename: "photo.jpg",
2710
+ requestId,
2711
+ size: 0,
2712
+ timestamp: new Date
2713
+ };
2714
+ pending.resolve(mockPhotoData);
2715
+ }
2689
2716
  return;
2690
2717
  }
2691
- const timeoutMs = 30000;
2692
- if (this.session && this.session.resources) {
2693
- this.session.resources.setTimeout(() => {
2694
- if (this.pendingPhotoRequests.has(requestId)) {
2695
- this.pendingPhotoRequests.get(requestId).reject("Photo request timed out");
2696
- this.pendingPhotoRequests.delete(requestId);
2697
- this.logger.warn({ requestId }, `\uD83D\uDCF8 Photo request timed out`);
2698
- }
2699
- }, timeoutMs);
2700
- } else {
2701
- setTimeout(() => {
2702
- if (this.pendingPhotoRequests.has(requestId)) {
2703
- this.pendingPhotoRequests.get(requestId).reject("Photo request timed out");
2704
- this.pendingPhotoRequests.delete(requestId);
2705
- this.logger.warn({ requestId }, `\uD83D\uDCF8 Photo request timed out`);
2706
- }
2707
- }, timeoutMs);
2708
- }
2709
2718
  } catch (error) {
2710
2719
  const errorMessage = error instanceof Error ? error.message : String(error);
2711
2720
  reject(`Failed to request photo: ${errorMessage}`);
2712
2721
  }
2713
2722
  });
2714
2723
  }
2715
- handlePhotoReceived(photoData) {
2716
- const { requestId } = photoData;
2717
- const pendingRequest = this.pendingPhotoRequests.get(requestId);
2718
- if (pendingRequest) {
2719
- this.logger.info({ requestId }, `\uD83D\uDCF8 Photo received for request ${requestId}`);
2720
- pendingRequest.resolve(photoData);
2721
- this.pendingPhotoRequests.delete(requestId);
2722
- } else {
2723
- this.logger.warn({ requestId }, `\uD83D\uDCF8 Received photo for unknown request ID: ${requestId}`);
2724
- }
2725
- }
2726
- handlePhotoError(errorResponse) {
2727
- const { requestId, error } = errorResponse;
2728
- const pendingRequest = this.pendingPhotoRequests.get(requestId);
2729
- if (pendingRequest) {
2730
- this.logger.error({ requestId, errorCode: error.code, errorMessage: error.message }, `\uD83D\uDCF8 Photo capture failed: ${error.code} - ${error.message}`);
2731
- pendingRequest.reject(`${error.code}: ${error.message}`);
2732
- this.pendingPhotoRequests.delete(requestId);
2733
- } else {
2734
- this.logger.warn({ requestId, errorCode: error.code, errorMessage: error.message }, `\uD83D\uDCF8 Received photo error for unknown request ID: ${requestId}`);
2735
- }
2736
- }
2737
2724
  hasPhotoPendingRequest(requestId) {
2738
- return this.pendingPhotoRequests.has(requestId);
2739
- }
2740
- getPhotoPendingRequestCount() {
2741
- return this.pendingPhotoRequests.size;
2742
- }
2743
- getPhotoPendingRequestIds() {
2744
- return Array.from(this.pendingPhotoRequests.keys());
2725
+ return this.session.appServer.getPhotoRequest(requestId) !== undefined;
2745
2726
  }
2746
2727
  cancelPhotoRequest(requestId) {
2747
- const pendingRequest = this.pendingPhotoRequests.get(requestId);
2748
- if (pendingRequest) {
2749
- pendingRequest.reject("Photo request cancelled");
2750
- this.pendingPhotoRequests.delete(requestId);
2728
+ const pending = this.session.appServer.completePhotoRequest(requestId);
2729
+ if (pending) {
2730
+ pending.reject(new Error("Photo request cancelled"));
2751
2731
  this.logger.info({ requestId }, `\uD83D\uDCF8 Photo request cancelled`);
2752
2732
  return true;
2753
2733
  }
2754
2734
  return false;
2755
2735
  }
2756
2736
  cancelAllPhotoRequests() {
2757
- const count = this.pendingPhotoRequests.size;
2758
- for (const [requestId, { reject }] of this.pendingPhotoRequests) {
2759
- reject("Photo request cancelled - session cleanup");
2760
- this.logger.info({ requestId }, `\uD83D\uDCF8 Photo request cancelled during cleanup`);
2761
- }
2762
- this.pendingPhotoRequests.clear();
2763
- return count;
2737
+ this.logger.debug(`\uD83D\uDCF8 cancelAllPhotoRequests called - cleanup now happens at AppServer level`);
2738
+ return 0;
2764
2739
  }
2765
2740
  async startStream(options) {
2766
2741
  this.logger.info({ rtmpUrl: options.rtmpUrl }, `\uD83D\uDCF9 RTMP stream request starting`);
@@ -3527,7 +3502,151 @@ class SimpleStorage {
3527
3502
  }
3528
3503
  }
3529
3504
 
3505
+ // src/utils/Observable.ts
3506
+ class Observable {
3507
+ _value;
3508
+ _listeners = new Set;
3509
+ _initialized = false;
3510
+ constructor(initialValue) {
3511
+ this._value = initialValue;
3512
+ }
3513
+ get value() {
3514
+ return this._value;
3515
+ }
3516
+ valueOf() {
3517
+ return this._value;
3518
+ }
3519
+ toString() {
3520
+ return String(this._value);
3521
+ }
3522
+ [Symbol.toPrimitive](hint) {
3523
+ if (hint === "string") {
3524
+ return String(this._value);
3525
+ }
3526
+ return this._value;
3527
+ }
3528
+ onChange(callback) {
3529
+ this._listeners.add(callback);
3530
+ if (this._initialized) {
3531
+ callback(this._value);
3532
+ }
3533
+ return () => this._listeners.delete(callback);
3534
+ }
3535
+ setValue(value) {
3536
+ const isFirstInit = !this._initialized;
3537
+ if (isFirstInit) {
3538
+ this._initialized = true;
3539
+ }
3540
+ if (isFirstInit || this._value !== value) {
3541
+ this._value = value;
3542
+ this._listeners.forEach((cb) => {
3543
+ try {
3544
+ cb(value);
3545
+ } catch (error) {
3546
+ console.error("Error in Observable onChange callback:", error);
3547
+ }
3548
+ });
3549
+ }
3550
+ }
3551
+ get listenerCount() {
3552
+ return this._listeners.size;
3553
+ }
3554
+ }
3555
+
3556
+ // src/app/session/device-state.ts
3557
+ class DeviceState {
3558
+ wifiConnected;
3559
+ wifiSsid;
3560
+ wifiLocalIp;
3561
+ batteryLevel;
3562
+ charging;
3563
+ caseBatteryLevel;
3564
+ caseCharging;
3565
+ caseOpen;
3566
+ caseRemoved;
3567
+ hotspotEnabled;
3568
+ hotspotSsid;
3569
+ connected;
3570
+ modelName;
3571
+ appSession;
3572
+ constructor(appSession) {
3573
+ this.appSession = appSession;
3574
+ this.wifiConnected = new Observable(false);
3575
+ this.wifiSsid = new Observable(null);
3576
+ this.wifiLocalIp = new Observable(null);
3577
+ this.batteryLevel = new Observable(null);
3578
+ this.charging = new Observable(null);
3579
+ this.caseBatteryLevel = new Observable(null);
3580
+ this.caseCharging = new Observable(null);
3581
+ this.caseOpen = new Observable(null);
3582
+ this.caseRemoved = new Observable(null);
3583
+ this.hotspotEnabled = new Observable(null);
3584
+ this.hotspotSsid = new Observable(null);
3585
+ this.connected = new Observable(false);
3586
+ this.modelName = new Observable(null);
3587
+ }
3588
+ updateFromMessage(state) {
3589
+ if (state.connected !== undefined) {
3590
+ this.connected.setValue(state.connected);
3591
+ }
3592
+ if (state.modelName !== undefined) {
3593
+ this.modelName.setValue(state.modelName);
3594
+ }
3595
+ if (state.wifiConnected !== undefined) {
3596
+ this.wifiConnected.setValue(state.wifiConnected);
3597
+ }
3598
+ if (state.wifiSsid !== undefined) {
3599
+ this.wifiSsid.setValue(state.wifiSsid ?? null);
3600
+ }
3601
+ if (state.wifiLocalIp !== undefined) {
3602
+ this.wifiLocalIp.setValue(state.wifiLocalIp ?? null);
3603
+ }
3604
+ if (state.batteryLevel !== undefined) {
3605
+ this.batteryLevel.setValue(state.batteryLevel ?? null);
3606
+ }
3607
+ if (state.charging !== undefined) {
3608
+ this.charging.setValue(state.charging ?? null);
3609
+ }
3610
+ if (state.caseBatteryLevel !== undefined) {
3611
+ this.caseBatteryLevel.setValue(state.caseBatteryLevel ?? null);
3612
+ }
3613
+ if (state.caseCharging !== undefined) {
3614
+ this.caseCharging.setValue(state.caseCharging ?? null);
3615
+ }
3616
+ if (state.caseOpen !== undefined) {
3617
+ this.caseOpen.setValue(state.caseOpen ?? null);
3618
+ }
3619
+ if (state.caseRemoved !== undefined) {
3620
+ this.caseRemoved.setValue(state.caseRemoved ?? null);
3621
+ }
3622
+ if (state.hotspotEnabled !== undefined) {
3623
+ this.hotspotEnabled.setValue(state.hotspotEnabled ?? null);
3624
+ }
3625
+ if (state.hotspotSsid !== undefined) {
3626
+ this.hotspotSsid.setValue(state.hotspotSsid ?? null);
3627
+ }
3628
+ }
3629
+ getSnapshot() {
3630
+ return {
3631
+ connected: this.connected.value,
3632
+ modelName: this.modelName.value ?? undefined,
3633
+ wifiConnected: this.wifiConnected.value,
3634
+ wifiSsid: this.wifiSsid.value ?? undefined,
3635
+ wifiLocalIp: this.wifiLocalIp.value ?? undefined,
3636
+ batteryLevel: this.batteryLevel.value ?? undefined,
3637
+ charging: this.charging.value ?? undefined,
3638
+ caseBatteryLevel: this.caseBatteryLevel.value ?? undefined,
3639
+ caseCharging: this.caseCharging.value ?? undefined,
3640
+ caseOpen: this.caseOpen.value ?? undefined,
3641
+ caseRemoved: this.caseRemoved.value ?? undefined,
3642
+ hotspotEnabled: this.hotspotEnabled.value ?? undefined,
3643
+ hotspotSsid: this.hotspotSsid.value ?? undefined
3644
+ };
3645
+ }
3646
+ }
3647
+
3530
3648
  // src/app/session/index.ts
3649
+ var SDK_SUBSCRIPTION_PATCH = "bug007-fix-v2";
3531
3650
  var APP_TO_APP_EVENT_TYPES = [
3532
3651
  "app_message_received",
3533
3652
  "app_user_joined",
@@ -3541,7 +3660,7 @@ class AppSession {
3541
3660
  ws = null;
3542
3661
  sessionId = null;
3543
3662
  reconnectAttempts = 0;
3544
- subscriptions = new Set;
3663
+ terminated = false;
3545
3664
  streamRates = new Map;
3546
3665
  resources = new ResourceTracker;
3547
3666
  settingsData = [];
@@ -3560,6 +3679,7 @@ class AppSession {
3560
3679
  led;
3561
3680
  audio;
3562
3681
  simpleStorage;
3682
+ device;
3563
3683
  appServer;
3564
3684
  logger;
3565
3685
  userId;
@@ -3609,18 +3729,14 @@ class AppSession {
3609
3729
  this.layouts = new LayoutManager(config.packageName, this.send.bind(this));
3610
3730
  this.settings = new SettingsManager(this.settingsData, this.config.packageName, this.config.mentraOSWebsocketUrl, this.sessionId ?? undefined, async (streams3) => {
3611
3731
  this.logger.debug({ streams: JSON.stringify(streams3) }, `[AppSession] subscribeFn called for streams`);
3612
- streams3.forEach((stream) => {
3613
- if (!this.subscriptions.has(stream)) {
3614
- this.subscriptions.add(stream);
3615
- this.logger.debug(`[AppSession] Auto-subscribed to stream '${stream}' for MentraOS setting.`);
3616
- } else {
3617
- this.logger.debug(`[AppSession] Already subscribed to stream '${stream}'.`);
3618
- }
3619
- });
3620
- this.logger.debug({ subscriptions: JSON.stringify(Array.from(this.subscriptions)) }, `[AppSession] Current subscriptions after subscribeFn`);
3732
+ const currentHandlerStreams = this.events.getRegisteredStreams();
3733
+ this.logger.debug({
3734
+ requestedStreams: JSON.stringify(streams3),
3735
+ currentHandlerStreams: JSON.stringify(currentHandlerStreams)
3736
+ }, `[AppSession] subscribeFn: requested streams vs current handler streams`);
3621
3737
  if (this.ws?.readyState === 1) {
3622
3738
  this.updateSubscriptions();
3623
- this.logger.debug(`[AppSession] Sent updated subscriptions to cloud after auto-subscribing to MentraOS setting.`);
3739
+ this.logger.debug(`[AppSession] Sent updated subscriptions to cloud (derived from handlers).`);
3624
3740
  } else {
3625
3741
  this.logger.debug(`[AppSession] WebSocket not open, will send subscriptions when connected.`);
3626
3742
  }
@@ -3631,6 +3747,7 @@ class AppSession {
3631
3747
  this.led = new LedModule(this, this.config.packageName, this.sessionId || "unknown-session-id", this.logger.child({ module: "led" }));
3632
3748
  this.audio = new AudioManager(this, this.config.packageName, this.sessionId || "unknown-session-id", this.logger.child({ module: "audio" }));
3633
3749
  this.simpleStorage = new SimpleStorage(this);
3750
+ this.device = { state: new DeviceState(this) };
3634
3751
  this.location = new LocationManager(this);
3635
3752
  }
3636
3753
  getSessionId() {
@@ -3697,7 +3814,6 @@ class AppSession {
3697
3814
  this.logger.warn(`[AppSession] Attempted to subscribe to App-to-App event type '${type}', which is not a valid stream. Use the event handler (e.g., onAppMessage) instead.`);
3698
3815
  return;
3699
3816
  }
3700
- this.subscriptions.add(type);
3701
3817
  if (rate) {
3702
3818
  this.streamRates.set(type, rate);
3703
3819
  }
@@ -3716,7 +3832,6 @@ class AppSession {
3716
3832
  this.logger.warn(`[AppSession] Attempted to unsubscribe from App-to-App event type '${type}', which is not a valid stream.`);
3717
3833
  return;
3718
3834
  }
3719
- this.subscriptions.delete(type);
3720
3835
  this.streamRates.delete(type);
3721
3836
  if (this.ws?.readyState === 1) {
3722
3837
  this.updateSubscriptions();
@@ -3840,11 +3955,15 @@ class AppSession {
3840
3955
  const isUserSessionEnded = reason && reason.includes("User session ended");
3841
3956
  this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] WebSocket closed with code ${code}${reasonStr}`);
3842
3957
  this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] isNormalClosure: ${isNormalClosure}, isManualStop: ${isManualStop}, isUserSessionEnded: ${isUserSessionEnded}`);
3843
- if (!isNormalClosure && !isManualStop) {
3958
+ if (isUserSessionEnded) {
3959
+ this.terminated = true;
3960
+ this.logger.info(`\uD83D\uDED1 [${this.config.packageName}] User session ended - marking as terminated, no reconnection allowed`);
3961
+ }
3962
+ if (!isNormalClosure && !isManualStop && !this.terminated) {
3844
3963
  this.logger.warn(`\uD83D\uDD0C [${this.config.packageName}] Abnormal closure detected, attempting reconnection`);
3845
3964
  this.handleReconnection();
3846
3965
  } else {
3847
- this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] Normal closure detected, not attempting reconnection`);
3966
+ this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] Normal/terminated closure detected, not attempting reconnection (terminated: ${this.terminated})`);
3848
3967
  }
3849
3968
  if (isUserSessionEnded) {
3850
3969
  this.logger.info(`\uD83D\uDED1 [${this.config.packageName}] User session ended - emitting disconnected event with sessionEnded flag`);
@@ -3908,7 +4027,26 @@ class AppSession {
3908
4027
  }
3909
4028
  });
3910
4029
  }
3911
- async disconnect() {
4030
+ async releaseOwnership(reason) {
4031
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
4032
+ this.logger.debug(`[${this.config.packageName}] Cannot release ownership - WebSocket not open`);
4033
+ return;
4034
+ }
4035
+ const message = {
4036
+ type: "ownership_release" /* OWNERSHIP_RELEASE */,
4037
+ packageName: this.config.packageName,
4038
+ sessionId: this.sessionId || "",
4039
+ reason,
4040
+ timestamp: new Date
4041
+ };
4042
+ this.logger.info({ reason, sessionId: this.sessionId }, `\uD83D\uDD04 [${this.config.packageName}] Releasing ownership: ${reason}`);
4043
+ this.send(message);
4044
+ await new Promise((resolve) => setTimeout(resolve, 100));
4045
+ }
4046
+ async disconnect(options) {
4047
+ if (options?.releaseOwnership && options?.reason) {
4048
+ await this.releaseOwnership(options.reason);
4049
+ }
3912
4050
  try {
3913
4051
  await this.simpleStorage.flush();
3914
4052
  console.log("SimpleStorage flushed on disconnect");
@@ -3924,7 +4062,6 @@ class AppSession {
3924
4062
  this.resources.dispose();
3925
4063
  this.ws = null;
3926
4064
  this.sessionId = null;
3927
- this.subscriptions.clear();
3928
4065
  this.reconnectAttempts = 0;
3929
4066
  }
3930
4067
  getSettings() {
@@ -3945,11 +4082,14 @@ class AppSession {
3945
4082
  if (!this.subscriptionSettingsHandler)
3946
4083
  return;
3947
4084
  try {
3948
- const newSubscriptions = this.subscriptionSettingsHandler(this.settingsData);
3949
- this.subscriptions.clear();
3950
- newSubscriptions.forEach((subscription) => {
3951
- this.subscriptions.add(subscription);
3952
- });
4085
+ const settingsSubscriptions = this.subscriptionSettingsHandler(this.settingsData);
4086
+ const handlerStreams = this.events.getRegisteredStreams();
4087
+ if (settingsSubscriptions.length !== handlerStreams.length) {
4088
+ this.logger.warn({
4089
+ settingsSubscriptions: JSON.stringify(settingsSubscriptions),
4090
+ handlerStreams: JSON.stringify(handlerStreams)
4091
+ }, `[AppSession] Settings-based subscriptions (${settingsSubscriptions.length}) differ from handler-based subscriptions (${handlerStreams.length}). ` + `Subscriptions are now derived from handlers. Ensure handlers are registered for desired streams.`);
4092
+ }
3953
4093
  if (this.ws && this.ws.readyState === 1) {
3954
4094
  this.updateSubscriptions();
3955
4095
  }
@@ -4079,6 +4219,8 @@ class AppSession {
4079
4219
  this.logger.debug(`[AppSession] No capabilities provided in CONNECTION_ACK`);
4080
4220
  }
4081
4221
  this.events.emit("connected", this.settingsData);
4222
+ const handlerCount = this.events.getRegisteredStreams().length;
4223
+ this.logger.info({ patch: SDK_SUBSCRIPTION_PATCH, handlerCount }, `[AppSession] \uD83D\uDD27 SDK Patch Active: ${SDK_SUBSCRIPTION_PATCH} - Subscriptions derived from ${handlerCount} handler(s)`);
4082
4224
  this.updateSubscriptions();
4083
4225
  if (this.shouldUpdateSubscriptionsOnSettingsChange && this.settingsData.length > 0) {
4084
4226
  this.updateSubscriptionsFromSettings();
@@ -4087,28 +4229,33 @@ class AppSession {
4087
4229
  const errorMessage = message.message || "Unknown connection error";
4088
4230
  this.events.emit("error", new Error(errorMessage));
4089
4231
  } else if (message.type === "audio_chunk" /* AUDIO_CHUNK */) {
4090
- if (this.subscriptions.has("audio_chunk" /* AUDIO_CHUNK */)) {
4232
+ const hasAudioHandler = this.events.getRegisteredStreams().includes("audio_chunk" /* AUDIO_CHUNK */);
4233
+ if (hasAudioHandler) {
4091
4234
  this.events.emit("audio_chunk" /* AUDIO_CHUNK */, message);
4092
4235
  }
4093
4236
  } else if (isDataStream(message) && message.streamType === "glasses_connection_state" /* GLASSES_CONNECTION_STATE */) {
4094
4237
  this.glassesConnectionState = message.data;
4095
- if (this.subscriptions.has("glasses_connection_state" /* GLASSES_CONNECTION_STATE */)) {
4238
+ const hasGlassesStateHandler = this.events.getRegisteredStreams().includes("glasses_connection_state" /* GLASSES_CONNECTION_STATE */);
4239
+ if (hasGlassesStateHandler) {
4096
4240
  const sanitizedData = this.sanitizeEventData("glasses_connection_state" /* GLASSES_CONNECTION_STATE */, message.data);
4097
4241
  this.events.emit("glasses_connection_state" /* GLASSES_CONNECTION_STATE */, sanitizedData);
4098
4242
  }
4099
4243
  } else if (isDataStream(message)) {
4100
4244
  const messageStreamType = message.streamType;
4101
- if (messageStreamType && this.subscriptions.has(messageStreamType)) {
4245
+ const hasHandler = this.events.getRegisteredStreams().includes(messageStreamType);
4246
+ if (messageStreamType && hasHandler) {
4102
4247
  const sanitizedData = this.sanitizeEventData(messageStreamType, message.data);
4103
4248
  this.events.emit(messageStreamType, sanitizedData);
4104
4249
  }
4105
4250
  } else if (isRtmpStreamStatus2(message)) {
4106
- if (this.subscriptions.has("rtmp_stream_status" /* RTMP_STREAM_STATUS */)) {
4251
+ const hasRtmpHandler = this.events.getRegisteredStreams().includes("rtmp_stream_status" /* RTMP_STREAM_STATUS */);
4252
+ if (hasRtmpHandler) {
4107
4253
  this.events.emit("rtmp_stream_status" /* RTMP_STREAM_STATUS */, message);
4108
4254
  }
4109
4255
  this.camera.updateStreamState(message);
4110
4256
  } else if (isManagedStreamStatus(message)) {
4111
- if (this.subscriptions.has("managed_stream_status" /* MANAGED_STREAM_STATUS */)) {
4257
+ const hasManagedStreamHandler = this.events.getRegisteredStreams().includes("managed_stream_status" /* MANAGED_STREAM_STATUS */);
4258
+ if (hasManagedStreamHandler) {
4112
4259
  this.events.emit("managed_stream_status" /* MANAGED_STREAM_STATUS */, message);
4113
4260
  }
4114
4261
  this.camera.handleManagedStreamStatus(message);
@@ -4139,6 +4286,12 @@ class AppSession {
4139
4286
  modelName: capabilitiesMessage.modelName,
4140
4287
  timestamp: capabilitiesMessage.timestamp
4141
4288
  });
4289
+ } else if (isDeviceStateUpdate(message)) {
4290
+ this.device.state.updateFromMessage(message.state);
4291
+ this.logger.debug({
4292
+ changedFields: Object.keys(message.state),
4293
+ fullSnapshot: message.fullSnapshot
4294
+ }, `[AppSession] Device state updated via WebSocket`);
4142
4295
  } else if (isAppStopped(message)) {
4143
4296
  const reason = message.reason || "unknown";
4144
4297
  const displayReason = `App stopped: ${reason}`;
@@ -4245,7 +4398,8 @@ class AppSession {
4245
4398
  }
4246
4399
  handleBinaryMessage(buffer) {
4247
4400
  try {
4248
- if (!this.subscriptions.has("audio_chunk" /* AUDIO_CHUNK */)) {
4401
+ const hasAudioHandler = this.events.getRegisteredStreams().includes("audio_chunk" /* AUDIO_CHUNK */);
4402
+ if (!hasAudioHandler) {
4249
4403
  return;
4250
4404
  }
4251
4405
  if (!buffer || buffer.byteLength === 0) {
@@ -4318,8 +4472,9 @@ class AppSession {
4318
4472
  this.send(message);
4319
4473
  }
4320
4474
  updateSubscriptions() {
4321
- this.logger.info({ subscriptions: JSON.stringify(Array.from(this.subscriptions)) }, `[AppSession] updateSubscriptions: sending subscriptions to cloud`);
4322
- const subscriptionPayload = Array.from(this.subscriptions).map((stream) => {
4475
+ const derivedSubscriptions = this.events.getRegisteredStreams();
4476
+ this.logger.info({ subscriptions: JSON.stringify(derivedSubscriptions) }, `[AppSession] updateSubscriptions: sending ${derivedSubscriptions.length} subscriptions to cloud (derived from handlers)`);
4477
+ const subscriptionPayload = derivedSubscriptions.map((stream) => {
4323
4478
  const rate = this.streamRates.get(stream);
4324
4479
  if (rate && stream === "location_stream" /* LOCATION_STREAM */) {
4325
4480
  return { stream: "location_stream", rate };
@@ -4336,6 +4491,10 @@ class AppSession {
4336
4491
  this.send(message);
4337
4492
  }
4338
4493
  async handleReconnection() {
4494
+ if (this.terminated) {
4495
+ this.logger.info(`\uD83D\uDD04 Reconnection skipped: session was terminated (User session ended). ` + `If cloud restarts app, onSession will be called with fresh handlers.`);
4496
+ return;
4497
+ }
4339
4498
  if (!this.config.autoReconnect || !this.sessionId) {
4340
4499
  this.logger.debug(`\uD83D\uDD04 Reconnection skipped: autoReconnect=${this.config.autoReconnect}, sessionId=${this.sessionId ? "valid" : "invalid"}`);
4341
4500
  return;
@@ -4415,7 +4574,12 @@ class AppSession {
4415
4574
  throw new Error(`Failed to send message: ${errorMessage}`);
4416
4575
  }
4417
4576
  } catch (error) {
4418
- this.logger.error(error, "Message send error");
4577
+ const isDisconnectError = error instanceof Error && (error.message.includes("WebSocket not connected") || error.message.includes("CLOSED") || error.message.includes("CLOSING"));
4578
+ if (isDisconnectError) {
4579
+ this.logger.debug(error, "Message send skipped - session disconnected");
4580
+ } else {
4581
+ this.logger.error(error, "Message send error");
4582
+ }
4419
4583
  if (error instanceof Error) {
4420
4584
  this.events.emit("error", error);
4421
4585
  } else {
@@ -4872,6 +5036,7 @@ class AppServer {
4872
5036
  activeSessionsByUserId = new Map;
4873
5037
  cleanupHandlers = [];
4874
5038
  appInstructions = null;
5039
+ pendingPhotoRequests = new Map;
4875
5040
  logger;
4876
5041
  constructor(config) {
4877
5042
  this.config = config;
@@ -4968,10 +5133,10 @@ class AppServer {
4968
5133
  });
4969
5134
  });
4970
5135
  }
4971
- stop() {
5136
+ async stop() {
4972
5137
  this.logger.info(`
4973
5138
  \uD83D\uDED1 Shutting down...`);
4974
- this.cleanup();
5139
+ await this.cleanup();
4975
5140
  process.exit(0);
4976
5141
  }
4977
5142
  generateToken(userId, sessionId, secretKey) {
@@ -5046,6 +5211,23 @@ class AppServer {
5046
5211
  this.logger.info({ userId }, `\uD83D\uDDE3️ Received session request for user ${userId}, session ${sessionId}
5047
5212
 
5048
5213
  `);
5214
+ const existingSession = this.activeSessions.get(sessionId);
5215
+ if (existingSession) {
5216
+ this.logger.info({ sessionId, userId }, `\uD83D\uDD04 Existing session found for ${sessionId} - sending OWNERSHIP_RELEASE and disconnecting before new connection`);
5217
+ try {
5218
+ await existingSession.releaseOwnership("switching_clouds");
5219
+ } catch (error) {
5220
+ this.logger.warn({ error, sessionId }, `⚠️ Failed to send OWNERSHIP_RELEASE to old session - continuing anyway`);
5221
+ }
5222
+ try {
5223
+ existingSession.disconnect();
5224
+ } catch (error) {
5225
+ this.logger.warn({ error, sessionId }, `⚠️ Failed to disconnect old session - continuing anyway`);
5226
+ }
5227
+ this.activeSessions.delete(sessionId);
5228
+ this.activeSessionsByUserId.delete(userId);
5229
+ this.logger.info({ sessionId, userId }, `✅ Old session cleaned up, proceeding with new connection`);
5230
+ }
5049
5231
  const session = new AppSession({
5050
5232
  packageName: this.config.packageName,
5051
5233
  apiKey: this.config.apiKey,
@@ -5054,25 +5236,48 @@ class AppServer {
5054
5236
  userId
5055
5237
  });
5056
5238
  const cleanupDisconnect = session.events.onDisconnected((info) => {
5239
+ let isPermanent = false;
5240
+ let reason = "unknown";
5057
5241
  if (typeof info === "string") {
5058
5242
  this.logger.info(`\uD83D\uDC4B Session ${sessionId} disconnected: ${info}`);
5243
+ reason = info;
5244
+ isPermanent = false;
5059
5245
  } else {
5060
5246
  this.logger.info(`\uD83D\uDC4B Session ${sessionId} disconnected: ${info.message} (code: ${info.code}, reason: ${info.reason})`);
5247
+ reason = info.reason || info.message;
5061
5248
  if (info.sessionEnded === true) {
5062
5249
  this.logger.info(`\uD83D\uDED1 User session ended for session ${sessionId}, calling onStop`);
5250
+ isPermanent = true;
5063
5251
  this.onStop(sessionId, userId, "User session ended").catch((error) => {
5064
5252
  this.logger.error(error, `❌ Error in onStop handler for session end:`);
5065
5253
  });
5066
5254
  } else if (info.permanent === true) {
5067
5255
  this.logger.info(`\uD83D\uDED1 Permanent disconnection detected for session ${sessionId}, calling onStop`);
5068
- const _session = this.activeSessions.get(sessionId);
5256
+ isPermanent = true;
5069
5257
  this.onStop(sessionId, userId, `Connection permanently lost: ${info.reason}`).catch((error) => {
5070
5258
  this.logger.error(error, `❌ Error in onStop handler for permanent disconnection:`);
5071
5259
  });
5260
+ } else if (info.wasClean === true || info.code === 1000 || info.code === 1001) {
5261
+ this.logger.info(`\uD83D\uDED1 Clean WebSocket closure for session ${sessionId} (code: ${info.code}), treating as permanent`);
5262
+ isPermanent = true;
5263
+ this.onStop(sessionId, userId, `Clean disconnect: ${reason}`).catch((error) => {
5264
+ this.logger.error(error, `❌ Error in onStop handler for clean disconnect:`);
5265
+ });
5072
5266
  }
5073
5267
  }
5074
- this.activeSessions.delete(sessionId);
5075
- this.activeSessionsByUserId.delete(userId);
5268
+ if (isPermanent) {
5269
+ if (this.activeSessions.get(sessionId) === session) {
5270
+ this.activeSessions.delete(sessionId);
5271
+ } else {
5272
+ this.logger.debug({ sessionId }, `\uD83D\uDD04 Session ${sessionId} cleanup skipped - a newer session has taken over`);
5273
+ }
5274
+ if (this.activeSessionsByUserId.get(userId) === session) {
5275
+ this.activeSessionsByUserId.delete(userId);
5276
+ }
5277
+ this.cleanupPhotoRequestsForSession(sessionId);
5278
+ } else {
5279
+ this.logger.debug({ sessionId, reason }, `\uD83D\uDD04 Temporary disconnect for session ${sessionId}, keeping in maps for reconnection`);
5280
+ }
5076
5281
  });
5077
5282
  const cleanupError = session.events.onError((error) => {
5078
5283
  this.logger.error(error, `❌ [Session ${sessionId}] Error:`);
@@ -5175,10 +5380,20 @@ class AppServer {
5175
5380
  process.on("SIGTERM", () => this.stop());
5176
5381
  process.on("SIGINT", () => this.stop());
5177
5382
  }
5178
- cleanup() {
5383
+ async cleanup() {
5384
+ this.logger.info(`\uD83D\uDD27 [LOCAL SDK] cleanup() called - NOT sending OWNERSHIP_RELEASE`);
5179
5385
  for (const [sessionId, session] of this.activeSessions) {
5180
- this.logger.info(`\uD83D\uDC4B Closing session ${sessionId}`);
5181
- session.disconnect();
5386
+ this.logger.info(`\uD83D\uDC4B Closing session ${sessionId} (no ownership release - cloud will resurrect)`);
5387
+ try {
5388
+ await session.disconnect({
5389
+ releaseOwnership: false
5390
+ });
5391
+ } catch (error) {
5392
+ this.logger.error(error, `Error during cleanup of session ${sessionId}`);
5393
+ try {
5394
+ await session.disconnect();
5395
+ } catch {}
5396
+ }
5182
5397
  }
5183
5398
  this.activeSessions.clear();
5184
5399
  this.activeSessionsByUserId.clear();
@@ -5203,7 +5418,6 @@ class AppServer {
5203
5418
  try {
5204
5419
  const { requestId, type, success, errorCode, errorMessage } = req.body;
5205
5420
  const photoFile = req.file;
5206
- console.log("Received photo response: ", req.body);
5207
5421
  this.logger.info({ requestId, type, success, errorCode }, `\uD83D\uDCF8 Received photo response: ${requestId} (type: ${type})`);
5208
5422
  if (!requestId) {
5209
5423
  this.logger.error("No requestId in photo response");
@@ -5212,24 +5426,18 @@ class AppServer {
5212
5426
  error: "No requestId provided"
5213
5427
  });
5214
5428
  }
5215
- const session = this.findSessionByPhotoRequestId(requestId);
5216
- if (!session) {
5217
- this.logger.warn({ requestId }, "No active session found for photo request");
5429
+ const pending = this.completePhotoRequest(requestId);
5430
+ if (!pending) {
5431
+ this.logger.warn({ requestId, pendingCount: this.pendingPhotoRequests.size }, "\uD83D\uDCF8 No pending request found for photo (may have timed out or session ended)");
5218
5432
  return res.status(404).json({
5219
5433
  success: false,
5220
- error: "No active session found for this photo request"
5434
+ error: "No pending request found for this photo (may have timed out or session ended)"
5221
5435
  });
5222
5436
  }
5223
5437
  if (type === "photo_error" || success === false) {
5224
- const errorResponse = {
5225
- requestId,
5226
- success: false,
5227
- error: {
5228
- code: errorCode || "UNKNOWN_ERROR",
5229
- message: errorMessage || "Unknown error occurred"
5230
- }
5231
- };
5232
- session.camera.handlePhotoError(errorResponse);
5438
+ const errorMsg = errorMessage || "Unknown error occurred";
5439
+ this.logger.info({ requestId, errorCode, errorMessage: errorMsg }, "\uD83D\uDCF8 Photo error received");
5440
+ pending.reject(new Error(`Photo capture failed: ${errorMsg} (code: ${errorCode || "UNKNOWN_ERROR"})`));
5233
5441
  return res.json({
5234
5442
  success: true,
5235
5443
  requestId,
@@ -5238,6 +5446,7 @@ class AppServer {
5238
5446
  }
5239
5447
  if (!photoFile) {
5240
5448
  this.logger.error({ requestId }, "No photo file in successful upload");
5449
+ pending.reject(new Error("No photo file provided for successful upload"));
5241
5450
  return res.status(400).json({
5242
5451
  success: false,
5243
5452
  error: "No photo file provided for successful upload"
@@ -5251,7 +5460,8 @@ class AppServer {
5251
5460
  size: photoFile.size,
5252
5461
  timestamp: new Date
5253
5462
  };
5254
- session.camera.handlePhotoReceived(photoData);
5463
+ this.logger.info({ requestId, size: photoFile.size, mimeType: photoFile.mimetype }, "\uD83D\uDCF8 Photo received successfully, resolving promise");
5464
+ pending.resolve(photoData);
5255
5465
  res.json({
5256
5466
  success: true,
5257
5467
  requestId,
@@ -5266,6 +5476,53 @@ class AppServer {
5266
5476
  }
5267
5477
  });
5268
5478
  }
5479
+ registerPhotoRequest(requestId, request) {
5480
+ const timeoutMs = 30000;
5481
+ const timeoutId = setTimeout(() => {
5482
+ const pending = this.pendingPhotoRequests.get(requestId);
5483
+ if (pending) {
5484
+ pending.reject(new Error("Photo request timed out"));
5485
+ this.pendingPhotoRequests.delete(requestId);
5486
+ this.logger.warn({ requestId }, "\uD83D\uDCF8 Photo request timed out");
5487
+ }
5488
+ }, timeoutMs);
5489
+ this.pendingPhotoRequests.set(requestId, {
5490
+ ...request,
5491
+ timeoutId
5492
+ });
5493
+ this.logger.debug({ requestId, userId: request.userId, sessionId: request.sessionId }, "\uD83D\uDCF8 Photo request registered at AppServer level");
5494
+ }
5495
+ getPhotoRequest(requestId) {
5496
+ return this.pendingPhotoRequests.get(requestId);
5497
+ }
5498
+ completePhotoRequest(requestId) {
5499
+ const pending = this.pendingPhotoRequests.get(requestId);
5500
+ if (pending) {
5501
+ if (pending.timeoutId) {
5502
+ clearTimeout(pending.timeoutId);
5503
+ }
5504
+ this.pendingPhotoRequests.delete(requestId);
5505
+ this.logger.debug({ requestId }, "\uD83D\uDCF8 Photo request completed");
5506
+ }
5507
+ return pending;
5508
+ }
5509
+ cleanupPhotoRequestsForSession(sessionId) {
5510
+ let cleanedCount = 0;
5511
+ for (const [requestId, pending] of this.pendingPhotoRequests) {
5512
+ if (pending.sessionId === sessionId) {
5513
+ if (pending.timeoutId) {
5514
+ clearTimeout(pending.timeoutId);
5515
+ }
5516
+ pending.reject(new Error("Session ended"));
5517
+ this.pendingPhotoRequests.delete(requestId);
5518
+ cleanedCount++;
5519
+ this.logger.debug({ requestId, sessionId }, "\uD83D\uDCF8 Photo request cleaned up (session ended)");
5520
+ }
5521
+ }
5522
+ if (cleanedCount > 0) {
5523
+ this.logger.info({ sessionId, cleanedCount }, "\uD83D\uDCF8 Cleaned up photo requests for ended session");
5524
+ }
5525
+ }
5269
5526
  setupMentraAuthRedirect() {
5270
5527
  this.app.get("/mentra-auth", (req, res) => {
5271
5528
  const authUrl = `https://account.mentra.glass/auth?packagename=${encodeURIComponent(this.config.packageName)}`;
@@ -5273,14 +5530,6 @@ class AppServer {
5273
5530
  res.redirect(302, authUrl);
5274
5531
  });
5275
5532
  }
5276
- findSessionByPhotoRequestId(requestId) {
5277
- for (const [_sessionId, session] of this.activeSessions) {
5278
- if (session.camera.hasPhotoPendingRequest(requestId)) {
5279
- return session;
5280
- }
5281
- }
5282
- return;
5283
- }
5284
5533
  }
5285
5534
 
5286
5535
  class TpaServer extends AppServer {
@@ -5301,6 +5550,8 @@ export {
5301
5550
  isValidLanguageCode,
5302
5551
  isVad,
5303
5552
  isUpdate,
5553
+ isUdpUnregister,
5554
+ isUdpRegister,
5304
5555
  isStreamStatusCheckResponse,
5305
5556
  isStreamCategory,
5306
5557
  isStopWebhookRequest,
@@ -5332,6 +5583,7 @@ export {
5332
5583
  isPhoneNotificationDismissed,
5333
5584
  isPhoneNotification,
5334
5585
  isPhoneBatteryUpdate,
5586
+ isOwnershipRelease,
5335
5587
  isMicrophoneStateChange,
5336
5588
  isManagedStreamStopRequest,
5337
5589
  isManagedStreamStatus,
@@ -5400,6 +5652,7 @@ export {
5400
5652
  PhotoStage,
5401
5653
  PhotoErrorCode,
5402
5654
  PermissionType,
5655
+ Observable,
5403
5656
  LedModule,
5404
5657
  LayoutType,
5405
5658
  LEGACY_PERMISSION_MAP,
@@ -5408,6 +5661,7 @@ export {
5408
5661
  GlassesToCloudMessageType,
5409
5662
  GIVE_APP_CONTROL_OF_TOOL_RESPONSE,
5410
5663
  EventTypes,
5664
+ DeviceState,
5411
5665
  DashboardMode,
5412
5666
  DashboardMessageTypes,
5413
5667
  ControlActionTypes,
@@ -5425,4 +5679,4 @@ export {
5425
5679
  AnimationUtils
5426
5680
  };
5427
5681
 
5428
- //# debugId=AD30D8A3207C691A64756E2164756E21
5682
+ //# debugId=82AABDD8D6AA68F464756E2164756E21