@mentra/engine 3.2.0-dev.261 → 3.2.0-dev.262

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 (33) hide show
  1. package/build/generated/releaseMetadata.js +5 -5
  2. package/build/generated/releaseMetadata.js.map +1 -1
  3. package/build/services/AcsMeetingService.d.ts +25 -1
  4. package/build/services/AcsMeetingService.d.ts.map +1 -1
  5. package/build/services/AcsMeetingService.js +41 -3
  6. package/build/services/AcsMeetingService.js.map +1 -1
  7. package/build/services/LocalMiniappRuntime.d.ts +32 -3
  8. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  9. package/build/services/LocalMiniappRuntime.js +284 -30
  10. package/build/services/LocalMiniappRuntime.js.map +1 -1
  11. package/build/services/MiniappLiveness.d.ts +11 -0
  12. package/build/services/MiniappLiveness.d.ts.map +1 -1
  13. package/build/services/MiniappLiveness.js +11 -0
  14. package/build/services/MiniappLiveness.js.map +1 -1
  15. package/build/services/PhoneStreamCoordinator.d.ts +13 -1
  16. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  17. package/build/services/PhoneStreamCoordinator.js +27 -4
  18. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  19. package/build/services/SoftapCallTransport.d.ts +87 -7
  20. package/build/services/SoftapCallTransport.d.ts.map +1 -1
  21. package/build/services/SoftapCallTransport.js +229 -28
  22. package/build/services/SoftapCallTransport.js.map +1 -1
  23. package/build/utils/miniappGlobals.d.ts.map +1 -1
  24. package/build/utils/miniappGlobals.js +13 -11
  25. package/build/utils/miniappGlobals.js.map +1 -1
  26. package/package.json +8 -8
  27. package/src/generated/releaseMetadata.ts +5 -5
  28. package/src/services/AcsMeetingService.ts +51 -3
  29. package/src/services/LocalMiniappRuntime.ts +296 -30
  30. package/src/services/MiniappLiveness.ts +18 -0
  31. package/src/services/PhoneStreamCoordinator.ts +27 -5
  32. package/src/services/SoftapCallTransport.ts +274 -32
  33. package/src/utils/miniappGlobals.ts +13 -11
@@ -423,6 +423,7 @@ declare class LocalMiniappRuntime {
423
423
  private checkpointSoftapAttempt;
424
424
  /** Publishes a checklist snapshot, unless this attempt has already been superseded. */
425
425
  private emitSoftapProgress;
426
+ private softapRecoveryFields;
426
427
  /**
427
428
  * Narrates preflight work on the first row while it is still pending.
428
429
  *
@@ -433,6 +434,23 @@ declare class LocalMiniappRuntime {
433
434
  */
434
435
  private narrateSoftapPreflight;
435
436
  private runSoftapAttempt;
437
+ /**
438
+ * SoftAP loss is a media outage. Keep ACS, destroy generation N, wait for the glasses,
439
+ * then build generation N+1. Duplicate losses do not renew the deadline.
440
+ */
441
+ private beginSoftapRecovery;
442
+ /**
443
+ * Wait for a link the hotspot commands can actually use.
444
+ *
445
+ * The hotspot goes away a couple of seconds before the phone declares the BLE link dead, so the
446
+ * snapshot at loss time still reads "ready". Taking that at face value sent `set_hotspot_state`
447
+ * into a dying link twice, and each one burned its full timeout before the wearer was even back.
448
+ * Requiring the link to hold steady turns that into waiting, which is what the budget is for.
449
+ */
450
+ private waitForGlassesReturn;
451
+ private awaitGlassesReady;
452
+ /** True when the link stays ready for [ms]; false the moment it drops again. */
453
+ private glassesLinkHoldsSteady;
436
454
  /**
437
455
  * Cancels a SoftAP attempt and does not return until everything it owns has been released.
438
456
  *
@@ -457,6 +475,17 @@ declare class LocalMiniappRuntime {
457
475
  * is what lets the next call start on leaked state.
458
476
  */
459
477
  private teardownSoftapAttempt;
478
+ /**
479
+ * Send one hotspot command, re-sending it once if a session refresh cancelled it.
480
+ *
481
+ * The retry lives inside the unit chained onto [glassesHotspotCommand] rather than around it,
482
+ * which is what holds the invariant: one logical hotspot operation issues at most two native
483
+ * commands, and no later operation runs between them. A retry outside the queue would let an
484
+ * `on` land between the two halves of an `off` and leave the glasses in the opposite state.
485
+ *
486
+ * Re-sending is free because ASG compares against current state first and answers a same-state
487
+ * command with its current status instead of cycling the AP.
488
+ */
460
489
  private setGlassesHotspotState;
461
490
  /**
462
491
  * The last two gates, and the point where a timeout stops being treated as consent.
@@ -468,9 +497,9 @@ declare class LocalMiniappRuntime {
468
497
  * failure — the listener was still inside its 3s tombstone and the glasses AP was still up.
469
498
  *
470
499
  * A bound that expires is not success. It means the resource is still held by a call nobody is
471
- * in any more, so it is taken by force and re-verified. Only if *that* fails does the next join
472
- * get refused, which is a worse outcome than a slow teardown and a better one than two calls
473
- * sharing a port.
500
+ * in any more, so it is taken by force and re-verified. An ingest port that is still held
501
+ * refuses the next join. A glasses hotspot that missed its off-ack does not the next join
502
+ * wants the AP up and will raise it again.
474
503
  *
475
504
  * @returns failures to record; a non-empty list refuses the next join.
476
505
  */
@@ -1 +1 @@
1
- {"version":3,"file":"LocalMiniappRuntime.d.ts","sourceRoot":"","sources":["../../src/services/LocalMiniappRuntime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"LocalMiniappRuntime.d.ts","sourceRoot":"","sources":["../../src/services/LocalMiniappRuntime.ts"],"names":[],"mappings":"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"}
@@ -23,7 +23,7 @@ import { storage as mmkvStorage } from "../utils/storage/storage";
23
23
  import { BgTimer } from "../utils/timers";
24
24
  import devServerBridge from "./DevServerBridge";
25
25
  import { islandNotifications } from "./NotificationsEmitter";
26
- import { isGlassesConnected } from "./GlassesReadiness";
26
+ import { isGlassesConnected, isGlassesReady, waitForGlassesReady } from "./GlassesReadiness";
27
27
  import { toMiniappConnectionData } from "./GlassesStatusProjection";
28
28
  import audioPlaybackService from "./AudioPlaybackService";
29
29
  import { phoneLocationService } from "./PhoneLocationService";
@@ -56,11 +56,11 @@ import { NavigationHandlers } from "./NavigationHandlers";
56
56
  import { useAppStatusStore } from "../stores/apps";
57
57
  import appRegistry, { getDevAppAttestation, getDevAppSourcePackage } from "./AppRegistry";
58
58
  import { resolveForegroundLocationPermission } from "./ForegroundLocationPermission";
59
- import { advanceMiniappPingLiveness } from "./MiniappLiveness";
59
+ import { advanceMiniappPingLiveness, shouldHoldMiniappPingLiveness } from "./MiniappLiveness";
60
60
  import { listPhoneCalendarEvents, PhoneCalendarError } from "./PhoneCalendarService";
61
61
  import { LocalMiniappStorage } from "./LocalMiniappStorage";
62
62
  import acsMeetingService, { parseAcsCallOrigin, parseAcsOutgoingVideo, parseAcsVideoSource, resolveAcsAudioSource, } from "./AcsMeetingService";
63
- import { createSoftapCallDeps, SoftapCallError, SoftapCallTransport, SOFTAP_STEPS, } from "./SoftapCallTransport";
63
+ import { createSoftapCallDeps, SoftapCallError, SoftapCallTransport, RETURN_DEADLINE_MS, SOFTAP_STEPS, } from "./SoftapCallTransport";
64
64
  import { awaitCleanupBarrier } from "./SoftapCleanupBarrier";
65
65
  import { softapTrace, softapTraceFailure } from "../utils/softapTrace";
66
66
  import { PermissionFeatures, permissions } from "../facades/permissions";
@@ -120,6 +120,11 @@ const FOREGROUND_LIVENESS_PROBE_TIMEOUT_MS = 2_500;
120
120
  * rather than sit in "connecting" while the wearer waits.
121
121
  */
122
122
  const SOFTAP_FIRST_FRAME_MS = 20_000;
123
+ /**
124
+ * How long the BLE link must stay up before SoftAP recovery trusts it. Covers the gap between
125
+ * the hotspot dying and the phone noticing the link died with it.
126
+ */
127
+ const GLASSES_LINK_SETTLE_MS = 4_000;
123
128
  const REQUEST_WIFI_SETUP_TYPE = "miniapp_request_wifi_setup";
124
129
  // Unregister after this many missed pongs. Generous on purpose: a busy
125
130
  // context (heavy interim translation traffic) or OS scheduling while idle can
@@ -265,6 +270,25 @@ const SOFTAP_INGEST_CLOSE_WAIT_MS = 3_500;
265
270
  const SOFTAP_HOTSPOT_OFF_ACK_MS = 5_000;
266
271
  /** After a forced close there is nothing left to wait for, so this only catches a real failure. */
267
272
  const SOFTAP_FORCED_VERIFY_MS = 500;
273
+ /**
274
+ * Rejection codes meaning the glasses replaced their Wi-Fi protocol session, not that the
275
+ * command failed.
276
+ *
277
+ * The glasses emit `wifi_protocol_session_ready` on every `glasses_ready`, which fires on BLE
278
+ * reconnects and ASG restarts — routine during a SoftAP teardown. The SDK cancels every pending
279
+ * Wi-Fi request when that lands, so a hotspot command in flight is lost even though nothing is
280
+ * actually wrong. `wifi_session_disconnected` is deliberately absent: that one is raised from the
281
+ * store update where the glasses went away, so there is nothing left to re-send to.
282
+ */
283
+ const HOTSPOT_SESSION_REFRESH_CODES = new Set(["wifi_session_restarted", "wifi_session_changed"]);
284
+ /**
285
+ * Let the restarted ASG command path settle before re-sending.
286
+ *
287
+ * `handleSetHotspotState` returns false when its network manager is not up yet, which drops the
288
+ * command silently, so an immediate re-send would just race the same initialization. Mirrors the
289
+ * retry spacing `disableHotspotWithRetry` uses for the sibling ASG-replacement case.
290
+ */
291
+ const HOTSPOT_SESSION_RETRY_DELAY_MS = 500;
268
292
  /** Reject with [reason] if [work] has not settled in [ms]. The work itself is not cancellable. */
269
293
  function withTimeout(work, ms, reason) {
270
294
  return new Promise((resolve, reject) => {
@@ -3097,6 +3121,7 @@ class LocalMiniappRuntime {
3097
3121
  this.sendToMiniapp(owner, {
3098
3122
  type: MiniappResponseType.MEETING_STATE,
3099
3123
  ...state,
3124
+ ...this.softapRecoveryFields(attempt?.packageName === owner ? attempt : null),
3100
3125
  });
3101
3126
  if (attempt?.ownsResources && attempt.transport?.shouldRepublish(state.mediaSource)) {
3102
3127
  void attempt.transport.republish(state.mediaSourceReason ?? "mediaSource failed");
@@ -3282,8 +3307,12 @@ class LocalMiniappRuntime {
3282
3307
  phase: "starting",
3283
3308
  steps: SOFTAP_STEPS.map((step) => ({ step, status: "pending" })),
3284
3309
  elapsedMs: 0,
3310
+ mediaGeneration: 0,
3285
3311
  },
3286
3312
  scopedLostUnsub: null,
3313
+ recovery: null,
3314
+ recoveryActive: false,
3315
+ recoveryDeadlineAt: null,
3287
3316
  body: null,
3288
3317
  teardown: null,
3289
3318
  settled,
@@ -3348,8 +3377,21 @@ class LocalMiniappRuntime {
3348
3377
  // join *is* in progress, and `idle` would make the miniapp think the call ended.
3349
3378
  state: current.state === "idle" ? "connecting" : current.state,
3350
3379
  softap: progress,
3380
+ ...this.softapRecoveryFields(attempt),
3351
3381
  });
3352
3382
  }
3383
+ softapRecoveryFields(attempt) {
3384
+ if (!attempt?.recoveryDeadlineAt)
3385
+ return {};
3386
+ return {
3387
+ recovery: {
3388
+ active: attempt.recoveryActive,
3389
+ generation: attempt.transport?.currentMediaGeneration(),
3390
+ deadlineAt: attempt.recoveryDeadlineAt,
3391
+ phase: attempt.transport?.currentPhase(),
3392
+ },
3393
+ };
3394
+ }
3353
3395
  /**
3354
3396
  * Narrates preflight work on the first row while it is still pending.
3355
3397
  *
@@ -3388,10 +3430,12 @@ class LocalMiniappRuntime {
3388
3430
  this.checkpointSoftapAttempt(attempt, "cleanup barrier");
3389
3431
  const cleanupError = this.softapCleanupError;
3390
3432
  if (cleanupError) {
3391
- // Report once, allowing a retry after the wearer power-cycles the glasses.
3433
+ // Cleared on read, so this refuses at most once and the next attempt always gets through —
3434
+ // which is why the wearer is told to retry rather than to go power-cycle anything. The
3435
+ // native reason stays in the trace: it names a leaked port, not something they can act on.
3392
3436
  this.softapCleanupError = null;
3393
3437
  softapTraceFailure("softap_join_refused", { packageName, reason: cleanupError });
3394
- throw new Error(`Previous call cleanup failed: ${cleanupError}. Power-cycle the glasses hotspot and try again.`);
3438
+ throw new Error("Previous call cleanup has not finished yet. Please try joining again.");
3395
3439
  }
3396
3440
  attempt.releaseHotspot = acquireGlassesHotspot();
3397
3441
  attempt.ownsResources = true;
@@ -3420,7 +3464,7 @@ class LocalMiniappRuntime {
3420
3464
  token: args.token,
3421
3465
  displayName: args.displayName,
3422
3466
  video: args.video,
3423
- awaitFirstFrame: () => acsMeetingService.waitForFirstFrame(SOFTAP_FIRST_FRAME_MS),
3467
+ awaitFirstFrame: (_report, options) => acsMeetingService.waitForFirstFrame(SOFTAP_FIRST_FRAME_MS, options),
3424
3468
  waitUntilLive: (timeoutMs) => acsMeetingService.waitUntilMediaLive(timeoutMs),
3425
3469
  subsystems: {
3426
3470
  setHotspotState: async (enabled) => {
@@ -3484,6 +3528,15 @@ class LocalMiniappRuntime {
3484
3528
  glassesLc3Uplink: () => acsMeetingService.glassesLc3UplinkActive(),
3485
3529
  startPublishing: (pkg, options) => phoneStreamCoordinator.startUnmanaged(pkg, options),
3486
3530
  stopPublishing: (pkg) => phoneStreamCoordinator.stop(pkg),
3531
+ discardPendingBleStop: () => phoneStreamCoordinator.discardPendingBleStop(),
3532
+ rebindIngest: async (report) => {
3533
+ report?.("Rebinding the glasses video receiver on the hotspot");
3534
+ const ingestUrl = await acsMeetingService.rebindSoftApIngest();
3535
+ if (!ingestUrl)
3536
+ throw new Error("the rebound meeting reported no ingest URL");
3537
+ report?.(`Receiver rebound at ${ingestUrl}`);
3538
+ return { ingestUrl };
3539
+ },
3487
3540
  },
3488
3541
  }));
3489
3542
  attempt.transport = transport;
@@ -3515,14 +3568,7 @@ class LocalMiniappRuntime {
3515
3568
  reason: error.message,
3516
3569
  attemptAgeMs: Date.now() - attempt.startedAt,
3517
3570
  });
3518
- const current = acsMeetingService.getState();
3519
- this.sendToMiniapp(packageName, {
3520
- type: MiniappResponseType.MEETING_STATE,
3521
- ...current,
3522
- state: "error",
3523
- error: `SOFTAP_NETWORK_LOST: ${error.message}`,
3524
- softap: transport.progress(),
3525
- });
3571
+ this.beginSoftapRecovery(attempt, transport, error);
3526
3572
  });
3527
3573
  // Sign in on the phone's current internet *before* the glasses hotspot takes DNS. On device,
3528
3574
  // createCallAgent after the scoped join timed out at 20s and only finished once SoftAP was
@@ -3561,6 +3607,163 @@ class LocalMiniappRuntime {
3561
3607
  });
3562
3608
  return state;
3563
3609
  }
3610
+ /**
3611
+ * SoftAP loss is a media outage. Keep ACS, destroy generation N, wait for the glasses,
3612
+ * then build generation N+1. Duplicate losses do not renew the deadline.
3613
+ */
3614
+ beginSoftapRecovery(attempt, transport, error) {
3615
+ // Gate on the live flag, not the promise: a settled Promise is still truthy, so a second
3616
+ // walk-away after a successful re-arm was dropped as "in-flight" and the glasses kept
3617
+ // POSTing WHIP at a hotspot the phone had already left.
3618
+ if (attempt.recoveryActive) {
3619
+ softapTrace("softap_recovery_dropped", { attempt: attempt.id, cause: "in-flight" });
3620
+ return;
3621
+ }
3622
+ if (attempt.cancelled || this.softapAttempt !== attempt) {
3623
+ softapTrace("softap_recovery_dropped", {
3624
+ attempt: attempt.id,
3625
+ cause: attempt.cancelled ? "retired" : "superseded",
3626
+ });
3627
+ return;
3628
+ }
3629
+ const lostAt = Date.now();
3630
+ attempt.recoveryDeadlineAt = lostAt + RETURN_DEADLINE_MS;
3631
+ attempt.recoveryActive = true;
3632
+ this.emitSoftapProgress(attempt, transport.progress());
3633
+ softapTrace("softap_recovery_begin", {
3634
+ attempt: attempt.id,
3635
+ deadlineAt: attempt.recoveryDeadlineAt,
3636
+ mediaGeneration: transport.currentMediaGeneration(),
3637
+ reason: error.message,
3638
+ });
3639
+ let run;
3640
+ run = (async () => {
3641
+ try {
3642
+ await transport.recover(`${error.code}: ${error.message}`, {
3643
+ wait: async () => {
3644
+ const remaining = (attempt.recoveryDeadlineAt ?? 0) - Date.now();
3645
+ if (remaining <= 0) {
3646
+ throw new SoftapCallError("hotspot", "RETURN_DEADLINE", "The glasses did not return in time");
3647
+ }
3648
+ const ready = await this.waitForGlassesReturn(remaining, () => attempt.cancelled);
3649
+ if (attempt.cancelled || this.softapAttempt !== attempt) {
3650
+ throw new SoftapCallError("hotspot", "CANCELLED", "SoftAP recovery was cancelled");
3651
+ }
3652
+ if (!ready) {
3653
+ throw new SoftapCallError("hotspot", "RETURN_DEADLINE", "The glasses did not return in time");
3654
+ }
3655
+ },
3656
+ });
3657
+ if (attempt.cancelled || this.softapAttempt !== attempt)
3658
+ return;
3659
+ this.emitSoftapProgress(attempt, transport.progress());
3660
+ const current = acsMeetingService.getState();
3661
+ this.sendToMiniapp(attempt.packageName, {
3662
+ type: MiniappResponseType.MEETING_STATE,
3663
+ ...current,
3664
+ softap: transport.progress(),
3665
+ ...this.softapRecoveryFields(attempt),
3666
+ });
3667
+ softapTrace("softap_recovery_live", {
3668
+ attempt: attempt.id,
3669
+ mediaGeneration: transport.currentMediaGeneration(),
3670
+ elapsedMs: Date.now() - lostAt,
3671
+ });
3672
+ }
3673
+ catch (recoveryError) {
3674
+ if (attempt.cancelled || this.softapAttempt !== attempt)
3675
+ return;
3676
+ const message = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
3677
+ softapTraceFailure("softap_recovery_failed", {
3678
+ attempt: attempt.id,
3679
+ reason: message,
3680
+ elapsedMs: Date.now() - lostAt,
3681
+ });
3682
+ const current = acsMeetingService.getState();
3683
+ this.sendToMiniapp(attempt.packageName, {
3684
+ type: MiniappResponseType.MEETING_STATE,
3685
+ ...current,
3686
+ state: "error",
3687
+ error: `SOFTAP_NETWORK_LOST: ${error.message}`,
3688
+ softap: transport.progress(),
3689
+ recovery: {
3690
+ active: false,
3691
+ generation: transport.currentMediaGeneration(),
3692
+ deadlineAt: attempt.recoveryDeadlineAt ?? undefined,
3693
+ phase: transport.currentPhase(),
3694
+ },
3695
+ });
3696
+ }
3697
+ finally {
3698
+ attempt.recoveryActive = false;
3699
+ if (attempt.recovery === run)
3700
+ attempt.recovery = null;
3701
+ }
3702
+ })();
3703
+ attempt.recovery = run;
3704
+ }
3705
+ /**
3706
+ * Wait for a link the hotspot commands can actually use.
3707
+ *
3708
+ * The hotspot goes away a couple of seconds before the phone declares the BLE link dead, so the
3709
+ * snapshot at loss time still reads "ready". Taking that at face value sent `set_hotspot_state`
3710
+ * into a dying link twice, and each one burned its full timeout before the wearer was even back.
3711
+ * Requiring the link to hold steady turns that into waiting, which is what the budget is for.
3712
+ */
3713
+ async waitForGlassesReturn(timeoutMs, cancelled) {
3714
+ const deadlineAt = Date.now() + timeoutMs;
3715
+ while (!cancelled()) {
3716
+ const remaining = deadlineAt - Date.now();
3717
+ if (remaining <= 0)
3718
+ return false;
3719
+ if (!isGlassesReady(useGlassesStore.getState().connection) && !(await this.awaitGlassesReady(remaining, cancelled))) {
3720
+ return false;
3721
+ }
3722
+ const settleMs = Math.min(GLASSES_LINK_SETTLE_MS, Math.max(0, deadlineAt - Date.now()));
3723
+ if (await this.glassesLinkHoldsSteady(settleMs, cancelled))
3724
+ return true;
3725
+ }
3726
+ return false;
3727
+ }
3728
+ awaitGlassesReady(timeoutMs, cancelled) {
3729
+ const controller = new AbortController();
3730
+ const poll = setInterval(() => {
3731
+ if (cancelled())
3732
+ controller.abort();
3733
+ }, 250);
3734
+ return waitForGlassesReady({
3735
+ getConnection: () => useGlassesStore.getState().connection,
3736
+ subscribe: (listener) => useGlassesStore.subscribe((state) => state.connection, listener),
3737
+ timeoutMs,
3738
+ signal: controller.signal,
3739
+ }).finally(() => clearInterval(poll));
3740
+ }
3741
+ /** True when the link stays ready for [ms]; false the moment it drops again. */
3742
+ glassesLinkHoldsSteady(ms, cancelled) {
3743
+ if (ms <= 0)
3744
+ return Promise.resolve(isGlassesReady(useGlassesStore.getState().connection));
3745
+ return new Promise((resolve) => {
3746
+ let done = false;
3747
+ const settle = (steady) => {
3748
+ if (done)
3749
+ return;
3750
+ done = true;
3751
+ clearTimeout(timer);
3752
+ clearInterval(poll);
3753
+ unsubscribe();
3754
+ resolve(steady);
3755
+ };
3756
+ const unsubscribe = useGlassesStore.subscribe((state) => state.connection, (connection) => {
3757
+ if (!isGlassesReady(connection))
3758
+ settle(false);
3759
+ });
3760
+ const poll = setInterval(() => {
3761
+ if (cancelled())
3762
+ settle(false);
3763
+ }, 250);
3764
+ const timer = setTimeout(() => settle(true), ms);
3765
+ });
3766
+ }
3564
3767
  /**
3565
3768
  * Cancels a SoftAP attempt and does not return until everything it owns has been released.
3566
3769
  *
@@ -3603,7 +3806,11 @@ class LocalMiniappRuntime {
3603
3806
  afterMs: SOFTAP_CLEANUP_STALL_LOG_MS,
3604
3807
  });
3605
3808
  }, SOFTAP_CLEANUP_STALL_LOG_MS);
3606
- void Promise.allSettled([attempt.teardown, attempt.body ?? Promise.resolve()]).then(() => {
3809
+ void Promise.allSettled([
3810
+ attempt.teardown,
3811
+ attempt.body ?? Promise.resolve(),
3812
+ attempt.recovery ?? Promise.resolve(),
3813
+ ]).then(() => {
3607
3814
  clearTimeout(stallWatchdog);
3608
3815
  attempt.markSettled();
3609
3816
  if (this.softapAttempt === attempt)
@@ -3690,8 +3897,17 @@ class LocalMiniappRuntime {
3690
3897
  // so this is the caller's business, not a reason to block the next call.
3691
3898
  endFailure = await releaseStep("transport.stop", () => transport.stop(mode ? { mode } : {}));
3692
3899
  const undoFailures = transport.lastTeardownFailures();
3693
- if (undoFailures.length > 0)
3694
- failures.push(`could not release ${undoFailures.join(", ")}`);
3900
+ // A hotspot that did not ACK off is leftover ON — the next join wants it on and will
3901
+ // call setHotspotState(true). Refusing that join is how we got hotspot_off_ack_timeout
3902
+ // as a wearer-facing "power-cycle the glasses" dead end.
3903
+ const blockingUndos = undoFailures.filter((step) => step !== "hotspot");
3904
+ if (blockingUndos.length > 0)
3905
+ failures.push(`could not release ${blockingUndos.join(", ")}`);
3906
+ if (undoFailures.includes("hotspot")) {
3907
+ console.warn("[LocalMiniappRuntime] glasses hotspot undo timed out; next join will raise it again", {
3908
+ attempt: attempt.id,
3909
+ });
3910
+ }
3695
3911
  }
3696
3912
  const scopedFailure = await releaseStep("leaveScopedNetwork", () => acsMeetingService.leaveScopedNetwork());
3697
3913
  if (scopedFailure) {
@@ -3791,8 +4007,32 @@ class LocalMiniappRuntime {
3791
4007
  if (endFailure)
3792
4008
  throw endFailure;
3793
4009
  }
4010
+ /**
4011
+ * Send one hotspot command, re-sending it once if a session refresh cancelled it.
4012
+ *
4013
+ * The retry lives inside the unit chained onto [glassesHotspotCommand] rather than around it,
4014
+ * which is what holds the invariant: one logical hotspot operation issues at most two native
4015
+ * commands, and no later operation runs between them. A retry outside the queue would let an
4016
+ * `on` land between the two halves of an `off` and leave the glasses in the opposite state.
4017
+ *
4018
+ * Re-sending is free because ASG compares against current state first and answers a same-state
4019
+ * command with its current status instead of cycling the AP.
4020
+ */
3794
4021
  setGlassesHotspotState(enabled) {
3795
- const run = this.glassesHotspotCommand.then(() => BluetoothSdk.setHotspotState(enabled), () => BluetoothSdk.setHotspotState(enabled));
4022
+ const send = async () => {
4023
+ try {
4024
+ return await BluetoothSdk.setHotspotState(enabled);
4025
+ }
4026
+ catch (error) {
4027
+ const code = error?.code;
4028
+ if (!code || !HOTSPOT_SESSION_REFRESH_CODES.has(code))
4029
+ throw error;
4030
+ softapTrace("softap_hotspot_session_retry", { enabled, code });
4031
+ await new Promise((resolve) => setTimeout(resolve, HOTSPOT_SESSION_RETRY_DELAY_MS));
4032
+ return await BluetoothSdk.setHotspotState(enabled);
4033
+ }
4034
+ };
4035
+ const run = this.glassesHotspotCommand.then(send, send);
3796
4036
  this.glassesHotspotCommand = run.then(() => undefined, () => undefined);
3797
4037
  return run;
3798
4038
  }
@@ -3806,9 +4046,9 @@ class LocalMiniappRuntime {
3806
4046
  * failure — the listener was still inside its 3s tombstone and the glasses AP was still up.
3807
4047
  *
3808
4048
  * A bound that expires is not success. It means the resource is still held by a call nobody is
3809
- * in any more, so it is taken by force and re-verified. Only if *that* fails does the next join
3810
- * get refused, which is a worse outcome than a slow teardown and a better one than two calls
3811
- * sharing a port.
4049
+ * in any more, so it is taken by force and re-verified. An ingest port that is still held
4050
+ * refuses the next join. A glasses hotspot that missed its off-ack does not the next join
4051
+ * wants the AP up and will raise it again.
3812
4052
  *
3813
4053
  * @returns failures to record; a non-empty list refuses the next join.
3814
4054
  */
@@ -3888,11 +4128,18 @@ class LocalMiniappRuntime {
3888
4128
  if (!gates.hotspotOff) {
3889
4129
  try {
3890
4130
  const status = await withTimeout(this.setGlassesHotspotState(false), SOFTAP_HOTSPOT_OFF_ACK_MS, "hotspot_off_ack_timeout");
3891
- if (status.state !== "disabled")
3892
- failures.push(`the glasses hotspot is still ${status.state}`);
4131
+ if (status.state !== "disabled") {
4132
+ console.warn("[LocalMiniappRuntime] glasses hotspot still reported after disable; next join will raise it", {
4133
+ attempt: attempt.id,
4134
+ state: status.state,
4135
+ });
4136
+ }
3893
4137
  }
3894
4138
  catch (error) {
3895
- failures.push(`glasses hotspot off: ${error instanceof Error ? error.message : String(error)}`);
4139
+ console.warn("[LocalMiniappRuntime] glasses hotspot off ack missed; next join will raise it", {
4140
+ attempt: attempt.id,
4141
+ reason: error instanceof Error ? error.message : String(error),
4142
+ });
3896
4143
  }
3897
4144
  }
3898
4145
  softapTrace("softap_teardown_forced_done", {
@@ -5032,13 +5279,20 @@ class LocalMiniappRuntime {
5032
5279
  doPingRound() {
5033
5280
  const toRemove = [];
5034
5281
  for (const [packageName, app] of this.connectedApps) {
5035
- const liveness = advanceMiniappPingLiveness(app.unansweredPingRounds, PING_TIMEOUT_THRESHOLD);
5036
- if (liveness.shouldUnregister) {
5037
- console.warn(`${LOG_TAG}: ${packageName} missed ${PING_TIMEOUT_THRESHOLD} pings, unregistering`);
5038
- toRemove.push(packageName);
5039
- continue;
5282
+ const holdPingLiveness = shouldHoldMiniappPingLiveness({
5283
+ packageName,
5284
+ softapPackageName: this.softapAttempt?.packageName,
5285
+ softapCancelled: this.softapAttempt?.cancelled,
5286
+ });
5287
+ if (!holdPingLiveness) {
5288
+ const liveness = advanceMiniappPingLiveness(app.unansweredPingRounds, PING_TIMEOUT_THRESHOLD);
5289
+ if (liveness.shouldUnregister) {
5290
+ console.warn(`${LOG_TAG}: ${packageName} missed ${PING_TIMEOUT_THRESHOLD} pings, unregistering`);
5291
+ toRemove.push(packageName);
5292
+ continue;
5293
+ }
5294
+ app.unansweredPingRounds = liveness.unansweredPingRounds;
5040
5295
  }
5041
- app.unansweredPingRounds = liveness.unansweredPingRounds;
5042
5296
  // Send PING — SDK auto-replies with PONG
5043
5297
  this.sendToMiniapp(packageName, {
5044
5298
  type: MiniappRequestType.PING,