@glassly/engine 0.3.5-beta.710 → 0.3.5-beta.760

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 (119) hide show
  1. package/build/engine.d.ts +4 -1
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +5 -1
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/ai.d.ts +3 -3
  6. package/build/facades/ai.d.ts.map +1 -1
  7. package/build/facades/ai.js +3 -3
  8. package/build/facades/ai.js.map +1 -1
  9. package/build/facades/glasses.d.ts +5 -1
  10. package/build/facades/glasses.d.ts.map +1 -1
  11. package/build/facades/glasses.js +5 -0
  12. package/build/facades/glasses.js.map +1 -1
  13. package/build/generated/releaseMetadata.js +3 -3
  14. package/build/generated/releaseMetadata.js.map +1 -1
  15. package/build/services/GestureDotsService.d.ts.map +1 -1
  16. package/build/services/GestureDotsService.js +2 -1
  17. package/build/services/GestureDotsService.js.map +1 -1
  18. package/build/services/GlassesTouchInput.d.ts +6 -4
  19. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  20. package/build/services/GlassesTouchInput.js +76 -37
  21. package/build/services/GlassesTouchInput.js.map +1 -1
  22. package/build/services/LocalDisplayManager.d.ts +12 -4
  23. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  24. package/build/services/LocalDisplayManager.js +21 -7
  25. package/build/services/LocalDisplayManager.js.map +1 -1
  26. package/build/services/LocalLlmModelManager.d.ts +3 -1
  27. package/build/services/LocalLlmModelManager.d.ts.map +1 -1
  28. package/build/services/LocalLlmModelManager.js +13 -5
  29. package/build/services/LocalLlmModelManager.js.map +1 -1
  30. package/build/services/LocalLlmPrompt.d.ts +10 -9
  31. package/build/services/LocalLlmPrompt.d.ts.map +1 -1
  32. package/build/services/LocalLlmPrompt.js +43 -32
  33. package/build/services/LocalLlmPrompt.js.map +1 -1
  34. package/build/services/LocalLlmProvider.d.ts.map +1 -1
  35. package/build/services/LocalLlmProvider.js +39 -20
  36. package/build/services/LocalLlmProvider.js.map +1 -1
  37. package/build/services/LocalMiniappRuntime.d.ts +22 -4
  38. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  39. package/build/services/LocalMiniappRuntime.js +108 -134
  40. package/build/services/LocalMiniappRuntime.js.map +1 -1
  41. package/build/services/MiniappExitGesture.d.ts +6 -3
  42. package/build/services/MiniappExitGesture.d.ts.map +1 -1
  43. package/build/services/MiniappExitGesture.js +4 -4
  44. package/build/services/MiniappExitGesture.js.map +1 -1
  45. package/build/services/MiniappWidgetMode.d.ts +6 -4
  46. package/build/services/MiniappWidgetMode.d.ts.map +1 -1
  47. package/build/services/MiniappWidgetMode.js +7 -6
  48. package/build/services/MiniappWidgetMode.js.map +1 -1
  49. package/build/services/SceneRenderer.d.ts.map +1 -1
  50. package/build/services/SceneRenderer.js +3 -1
  51. package/build/services/SceneRenderer.js.map +1 -1
  52. package/build/services/actionInvoke.d.ts +37 -0
  53. package/build/services/actionInvoke.d.ts.map +1 -0
  54. package/build/services/actionInvoke.js +89 -0
  55. package/build/services/actionInvoke.js.map +1 -0
  56. package/build/services/appleWatch/AppleWatchLink.d.ts +42 -0
  57. package/build/services/appleWatch/AppleWatchLink.d.ts.map +1 -0
  58. package/build/services/appleWatch/AppleWatchLink.js +259 -0
  59. package/build/services/appleWatch/AppleWatchLink.js.map +1 -0
  60. package/build/services/controllers.d.ts.map +1 -1
  61. package/build/services/controllers.js +16 -0
  62. package/build/services/controllers.js.map +1 -1
  63. package/build/services/settingsBridge.d.ts +4 -4
  64. package/build/services/settingsBridge.d.ts.map +1 -1
  65. package/build/services/settingsBridge.js +9 -4
  66. package/build/services/settingsBridge.js.map +1 -1
  67. package/build/stores/apps.d.ts.map +1 -1
  68. package/build/stores/apps.js +13 -11
  69. package/build/stores/apps.js.map +1 -1
  70. package/build/stores/bluetoothSettingKeys.d.ts +2 -0
  71. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  72. package/build/stores/bluetoothSettingKeys.js +17 -0
  73. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  74. package/build/stores/settings.d.ts.map +1 -1
  75. package/build/stores/settings.js +32 -2
  76. package/build/stores/settings.js.map +1 -1
  77. package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
  78. package/build/types/capabilities/rayneo-io.js +2 -0
  79. package/build/types/capabilities/rayneo-io.js.map +1 -1
  80. package/build/types/enums.d.ts +2 -1
  81. package/build/types/enums.d.ts.map +1 -1
  82. package/build/types/enums.js +1 -0
  83. package/build/types/enums.js.map +1 -1
  84. package/build/utils/display/scene/index.d.ts +1 -1
  85. package/build/utils/display/scene/index.d.ts.map +1 -1
  86. package/build/utils/display/scene/index.js.map +1 -1
  87. package/build/utils/display/scene/process.d.ts.map +1 -1
  88. package/build/utils/display/scene/process.js +20 -1
  89. package/build/utils/display/scene/process.js.map +1 -1
  90. package/build/utils/display/scene/types.d.ts +1 -1
  91. package/build/utils/display/scene/types.d.ts.map +1 -1
  92. package/build/utils/display/scene/types.js.map +1 -1
  93. package/package.json +7 -7
  94. package/src/engine.ts +5 -1
  95. package/src/facades/ai.ts +3 -3
  96. package/src/facades/glasses.ts +7 -0
  97. package/src/generated/releaseMetadata.ts +3 -3
  98. package/src/services/GestureDotsService.ts +2 -1
  99. package/src/services/GlassesTouchInput.ts +81 -34
  100. package/src/services/LocalDisplayManager.ts +22 -8
  101. package/src/services/LocalLlmModelManager.ts +16 -6
  102. package/src/services/LocalLlmPrompt.ts +56 -37
  103. package/src/services/LocalLlmProvider.ts +49 -22
  104. package/src/services/LocalMiniappRuntime.ts +140 -134
  105. package/src/services/MiniappExitGesture.ts +8 -5
  106. package/src/services/MiniappWidgetMode.ts +9 -7
  107. package/src/services/SceneRenderer.ts +3 -1
  108. package/src/services/actionInvoke.ts +105 -0
  109. package/src/services/appleWatch/AppleWatchLink.ts +297 -0
  110. package/src/services/controllers.ts +17 -0
  111. package/src/services/settingsBridge.ts +9 -4
  112. package/src/stores/apps.ts +12 -10
  113. package/src/stores/bluetoothSettingKeys.ts +20 -0
  114. package/src/stores/settings.ts +34 -2
  115. package/src/types/capabilities/rayneo-io.ts +2 -0
  116. package/src/types/enums.ts +1 -0
  117. package/src/utils/display/scene/index.ts +1 -0
  118. package/src/utils/display/scene/process.ts +17 -1
  119. package/src/utils/display/scene/types.ts +1 -0
@@ -82,6 +82,7 @@ import { invokeScanQrSeam } from "../runtime/scanQrSeam";
82
82
  import { invokeWebLoginSeam } from "../runtime/webLoginSeam";
83
83
  import { normalizeStreamAudioConfig, normalizeStreamVideoConfig } from "../runtime/streamConfig";
84
84
  import { visibleActionRows } from "./manifestActions";
85
+ import { actionFailure, appDeclaresAction, checkActionAccess, PendingActionCalls, } from "./actionInvoke";
85
86
  import { toLanguageHint } from "@glassly/cloud-protocol/languages";
86
87
  import { buildMiniappManifestSnapshot } from "../utils/miniappDiagnostics";
87
88
  import ttsModelManager from "./TTSModelManager";
@@ -438,6 +439,10 @@ class LocalMiniappRuntime {
438
439
  connectedApps = new Map();
439
440
  /** Claimed gestures (CLAIM_GESTURES): a stack per gesture, the most recent claimer holding it. */
440
441
  gestureClaims = emptyGestureClaims();
442
+ // The last main-view frame each app asked to render. Widget mode refuses
443
+ // frames and demotion clears them, and an app that has drawn once may not
444
+ // draw again on its own, so promotion replays this.
445
+ retainedRenders = new Map();
441
446
  /**
442
447
  * Packages that have completed their CONNECT handshake (sent
443
448
  * `miniapp_connect`, got `CONNECT_ACK`). Distinct from `connectedApps`,
@@ -490,7 +495,14 @@ class LocalMiniappRuntime {
490
495
  // secret never leaves the device.
491
496
  localSecret = `miniapp_${Date.now()}_${Math.random().toString(36).slice(2, 14)}`;
492
497
  usedTokens = new Set();
493
- constructor() { }
498
+ constructor() {
499
+ miniappWidgetMode.subscribe((packageName, widgetOnly) => {
500
+ // Promotion happens inside the apps store's start(); the core-app slot
501
+ // settles with that store write, so replay once it has.
502
+ if (!widgetOnly)
503
+ queueMicrotask(() => this.replayRetainedRender(packageName));
504
+ });
505
+ }
494
506
  /** Snapshot of miniapps actively subscribed to hardware button events. */
495
507
  getButtonPressSubscribers() {
496
508
  return [...(this.streamSubscribers.get(MiniappStreamType.BUTTON_PRESS) ?? [])].sort();
@@ -874,7 +886,11 @@ class LocalMiniappRuntime {
874
886
  // window ends early once this app pushes its first display (see
875
887
  // LocalDisplayManager.request), or after ~1.5s. Fires once per spawn —
876
888
  // including a crash-respawn, since each spawn is a fresh "app is starting".
877
- localDisplayManager.onMount(packageName, installedManifest?.name ?? packageName);
889
+ // A spawn for a dashboard widget is not the wearer starting the app: it draws
890
+ // nothing of its own, so it shows no boot text either.
891
+ if (!miniappWidgetMode.isWidgetOnly(packageName)) {
892
+ localDisplayManager.onMount(packageName, installedManifest?.name ?? packageName);
893
+ }
878
894
  }
879
895
  /**
880
896
  * Resolve once `packageName` has completed its CONNECT handshake. If it has
@@ -996,6 +1012,7 @@ class LocalMiniappRuntime {
996
1012
  }
997
1013
  // Its gesture claims go back to whoever claimed them before.
998
1014
  this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName));
1015
+ this.retainedRenders.delete(packageName);
999
1016
  // Warm-up is a request-owned camera lease. Cancel it even if the app record was already
1000
1017
  // removed so a close racing the camera-open promise cannot leave the sensor running.
1001
1018
  // Keep camera teardown ordered: restoring FOV can restart the HAL, so the warm-up lease must
@@ -2124,11 +2141,6 @@ class LocalMiniappRuntime {
2124
2141
  this.sendResult(packageName, requestId, true, { status: "blocked", reason: "app stopped" });
2125
2142
  return;
2126
2143
  }
2127
- // A widget-only app draws through its widget alone.
2128
- if (miniappWidgetMode.isWidgetOnly(packageName)) {
2129
- this.sendResult(packageName, requestId, true, { status: "blocked", reason: "widget mode" });
2130
- return;
2131
- }
2132
2144
  if (!Array.isArray(payload.elements)) {
2133
2145
  this.sendResult(packageName, requestId, false, undefined, {
2134
2146
  code: MiniappErrorCode.INTERNAL,
@@ -2136,6 +2148,17 @@ class LocalMiniappRuntime {
2136
2148
  });
2137
2149
  return;
2138
2150
  }
2151
+ if (payload.view !== "dashboard") {
2152
+ if (payload.elements.length === 0)
2153
+ this.retainedRenders.delete(packageName);
2154
+ else
2155
+ this.retainedRenders.set(packageName, payload);
2156
+ }
2157
+ // A widget-only app draws through its widget alone; the frame waits for promotion.
2158
+ if (miniappWidgetMode.isWidgetOnly(packageName)) {
2159
+ this.sendResult(packageName, requestId, true, { status: "blocked", reason: "widget mode" });
2160
+ return;
2161
+ }
2139
2162
  // The dashboard view is the overlay app's alone; anyone else's frame
2140
2163
  // is dropped, reported rather than silently lost.
2141
2164
  if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
@@ -2160,6 +2183,13 @@ class LocalMiniappRuntime {
2160
2183
  });
2161
2184
  }
2162
2185
  }
2186
+ /** Put a promoted app's last main-view frame on the glasses; widget mode had refused it. */
2187
+ replayRetainedRender(packageName) {
2188
+ const payload = this.retainedRenders.get(packageName);
2189
+ if (!payload || !this.connectedApps.has(packageName) || miniappWidgetMode.isWidgetOnly(packageName))
2190
+ return;
2191
+ this.handleRender(packageName, payload);
2192
+ }
2163
2193
  /**
2164
2194
  * display.showView: the active dashboard takes or releases the overlay
2165
2195
  * slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
@@ -5224,7 +5254,7 @@ class LocalMiniappRuntime {
5224
5254
  /** Max serialized size of an action call's params / result (256 KB). */
5225
5255
  static ACTION_PAYLOAD_CAP = 256 * 1024;
5226
5256
  /** Outstanding action invocations, keyed by host-generated callId. */
5227
- actionCalls = new Map();
5257
+ actionCalls = new PendingActionCalls(BgTimer, (value) => this.actionPayloadTooLarge(value));
5228
5258
  actionCallSeq = 0;
5229
5259
  interopApps() {
5230
5260
  return useAppStatusStore.getState().apps;
@@ -5564,10 +5594,12 @@ class LocalMiniappRuntime {
5564
5594
  }
5565
5595
  const provider = app.packageName;
5566
5596
  // Record the placement before the wake so the spawn lands in widget mode
5567
- // and an unplace racing it cannot stop the provider early.
5597
+ // and an unplace racing it cannot stop the provider early. A provider the
5598
+ // store already runs (a wearer's launch still spawning, say) stays a
5599
+ // normal run; the registry alone lags that launch.
5568
5600
  this.widgetPlacements = placeWidget(this.widgetPlacements, { hostPackageName, providerPackageName: provider, widgetId, box });
5569
5601
  miniappWidgetMode.setPlaced(provider, true);
5570
- if (!miniappRunningRegistry.has(provider))
5602
+ if (!app.running && !miniappRunningRegistry.has(provider))
5571
5603
  miniappWidgetMode.setWidgetOnly(provider, true);
5572
5604
  try {
5573
5605
  await this.wakeInteropApp(provider);
@@ -5754,104 +5786,82 @@ class LocalMiniappRuntime {
5754
5786
  return false;
5755
5787
  }
5756
5788
  }
5789
+ /** Floored at 6s: a freshly-woken target buffers an undelivered ACTION_CALL for 5s (HANDLER_WAIT_MS in @glassly/miniapp's actions module) + 1s for the reply. Keep in sync. */
5790
+ static clampActionTimeout(raw) {
5791
+ return Math.min(Math.max(Number(raw) || 30_000, 6_000), 120_000);
5792
+ }
5793
+ /** Caller identity the host presents for host-initiated actions (a system caller). */
5794
+ static HOST_ACTION_CALLER = "host";
5795
+ /** Installed, compatible, and declares the action — the same gates `invokeActionFromHost` applies up front. */
5796
+ hostActionAvailable(targetPackageName, actionId) {
5797
+ return appDeclaresAction(this.interopApps().find((a) => a.packageName === targetPackageName), actionId);
5798
+ }
5757
5799
  /**
5758
- * session.actions.invoke → wake the target headlessly, deliver an ACTION_CALL,
5759
- * and correlate its ACTION_RESULT back to the caller's pending request.
5800
+ * Host-initiated action call (e.g. the watch remote): same gates, headless
5801
+ * wake and ACTION_CALL/ACTION_RESULT correlation as a miniapp's
5802
+ * session.actions.invoke, with the host as a system caller.
5803
+ * Rejects with an Error whose `code` is a MiniappErrorCode.
5760
5804
  */
5805
+ async invokeActionFromHost(targetPackageName, actionId, params = {}, timeoutMs) {
5806
+ const outcome = await this.runActionInvoke(LocalMiniappRuntime.HOST_ACTION_CALLER, true, targetPackageName, actionId, params, LocalMiniappRuntime.clampActionTimeout(timeoutMs));
5807
+ if (outcome.ok)
5808
+ return outcome.data;
5809
+ throw Object.assign(new Error(outcome.error.message), { code: outcome.error.code });
5810
+ }
5811
+ /** session.actions.invoke → run the call, then relay the outcome to the caller's pending request. */
5761
5812
  async handleActionInvoke(callerPackageName, payload, requestId) {
5762
- const target = payload.targetPackageName;
5763
- const actionId = payload.actionId;
5764
- const params = payload.params ?? {};
5765
- // Floor the timeout at 6s so the host never rejects with ACTION_TIMEOUT before
5766
- // the target SDK's handler-registration window closes. A freshly-woken miniapp
5767
- // buffers an undelivered ACTION_CALL for HANDLER_WAIT_MS (5s in @glassly/miniapp's
5768
- // actions module) waiting for session.actions.handle; a shorter host timeout
5769
- // could fire while the target is still registering, then the action would run
5770
- // with no caller left to receive its result. 6s = that 5s buffer + 1s for the
5771
- // ACTION_RESULT/NO_ACTION_HANDLER reply to travel back. Keep these in sync.
5772
- const timeoutMs = Math.min(Math.max(Number(payload.timeoutMs) || 30_000, 6_000), 120_000);
5813
+ const outcome = await this.runActionInvoke(callerPackageName, this.isSystemPackage(callerPackageName), payload.targetPackageName, payload.actionId, payload.params ?? {}, LocalMiniappRuntime.clampActionTimeout(payload.timeoutMs));
5814
+ if (outcome.ok)
5815
+ this.sendResult(callerPackageName, requestId, true, outcome.data);
5816
+ else
5817
+ this.sendResult(callerPackageName, requestId, false, undefined, outcome.error);
5818
+ }
5819
+ /**
5820
+ * Gate, wake the target headlessly, deliver an ACTION_CALL, and resolve when
5821
+ * its ACTION_RESULT (or the timeout) arrives. Never rejects.
5822
+ */
5823
+ async runActionInvoke(callerPackageName, callerIsSystem, target, actionId, params, timeoutMs) {
5824
+ const fail = actionFailure;
5773
5825
  if (this.actionPayloadTooLarge(params)) {
5774
- this.sendResult(callerPackageName, requestId, false, undefined, {
5775
- code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
5776
- message: "action params exceeded the 256 KB cap",
5777
- });
5778
- return;
5826
+ return fail(MiniappErrorCode.PAYLOAD_TOO_LARGE, "action params exceeded the 256 KB cap");
5779
5827
  }
5780
5828
  const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined;
5781
- if (!target || !app) {
5782
- this.sendResult(callerPackageName, requestId, false, undefined, {
5783
- code: MiniappErrorCode.APP_NOT_FOUND,
5784
- message: `Miniapp not found: ${target ?? "(missing targetPackageName)"}`,
5785
- });
5786
- return;
5787
- }
5788
- if (!actionId) {
5789
- this.sendResult(callerPackageName, requestId, false, undefined, {
5790
- code: MiniappErrorCode.ACTION_NOT_FOUND,
5791
- message: "invoke requires an actionId",
5792
- });
5793
- return;
5794
- }
5795
- if (app.compatibility && app.compatibility.isCompatible === false) {
5796
- this.sendResult(callerPackageName, requestId, false, undefined, {
5797
- code: MiniappErrorCode.APP_NOT_COMPATIBLE,
5798
- message: `${target} is not compatible with the connected glasses`,
5799
- });
5800
- islandNotifications.emit({
5801
- kind: "version_incompatible",
5802
- packageName: target,
5803
- reason: `${target} is not compatible with the connected glasses`,
5804
- metadata: { missingRequired: app.compatibility.missingRequired ?? [], via: "miniapps.invoke" },
5805
- timestamp: Date.now(),
5806
- });
5807
- return;
5808
- }
5809
- // Declared-action gate: the target must declare the action in its manifest.
5810
- const declared = (app.actions ?? []).find((a) => a.id === actionId);
5811
- if (!declared) {
5812
- this.sendResult(callerPackageName, requestId, false, undefined, {
5813
- code: MiniappErrorCode.ACTION_NOT_FOUND,
5814
- message: `${target} does not declare action "${actionId}"`,
5815
- });
5816
- return;
5817
- }
5818
- // Visibility gate: declared actions are public by default; ones marked
5819
- // `visibility: "system"` stay restricted to system/dev callers.
5820
- if (declared.visibility === "system" && !this.isSystemPackage(callerPackageName)) {
5821
- this.auditInterop({
5822
- caller: callerPackageName,
5823
- op: "invoke",
5824
- target,
5825
- actionId,
5826
- ok: false,
5827
- errorCode: MiniappErrorCode.NOT_PERMITTED,
5828
- });
5829
- this.sendResult(callerPackageName, requestId, false, undefined, {
5830
- code: MiniappErrorCode.NOT_PERMITTED,
5831
- message: `Action "${actionId}" on ${target} is restricted to system apps`,
5832
- });
5833
- return;
5829
+ const denied = checkActionAccess(app, target, actionId, callerIsSystem);
5830
+ if (denied) {
5831
+ if (denied.code === MiniappErrorCode.APP_NOT_COMPATIBLE && target && app) {
5832
+ islandNotifications.emit({
5833
+ kind: "version_incompatible",
5834
+ packageName: target,
5835
+ reason: denied.message,
5836
+ metadata: { missingRequired: app.compatibility?.missingRequired ?? [], via: "miniapps.invoke" },
5837
+ timestamp: Date.now(),
5838
+ });
5839
+ }
5840
+ else if (denied.code === MiniappErrorCode.NOT_PERMITTED) {
5841
+ this.auditInterop({
5842
+ caller: callerPackageName,
5843
+ op: "invoke",
5844
+ target,
5845
+ actionId,
5846
+ ok: false,
5847
+ errorCode: MiniappErrorCode.NOT_PERMITTED,
5848
+ });
5849
+ }
5850
+ return { ok: false, error: denied };
5834
5851
  }
5852
+ if (!target || !actionId)
5853
+ return fail(MiniappErrorCode.INTERNAL, "unreachable");
5835
5854
  // Headless wake + wait for CONNECT (idempotent / fast if already connected).
5836
5855
  try {
5837
5856
  await this.wakeInteropApp(target);
5838
5857
  }
5839
5858
  catch (e) {
5840
- this.sendResult(callerPackageName, requestId, false, undefined, {
5841
- code: MiniappErrorCode.WAKE_FAILED,
5842
- message: e?.message ?? `failed to wake ${target}`,
5843
- });
5844
- return;
5859
+ return fail(MiniappErrorCode.WAKE_FAILED, e?.message ?? `failed to wake ${target}`);
5845
5860
  }
5846
5861
  // The wake waited for CONNECT, but the target could have dropped in the gap
5847
5862
  // before delivery — fail fast rather than arming a timer for a call that
5848
- // sendToMiniapp would silently drop (the caller would otherwise wait the
5849
- // full invoke timeout for an ACTION_CALL that was never delivered).
5863
+ // sendToMiniapp would silently drop.
5850
5864
  if (!this.connectedApps.has(target)) {
5851
- this.sendResult(callerPackageName, requestId, false, undefined, {
5852
- code: MiniappErrorCode.WAKE_FAILED,
5853
- message: `${target} disconnected before the action could be delivered`,
5854
- });
5855
5865
  this.auditInterop({
5856
5866
  caller: callerPackageName,
5857
5867
  op: "invoke",
@@ -5860,18 +5870,11 @@ class LocalMiniappRuntime {
5860
5870
  ok: false,
5861
5871
  errorCode: MiniappErrorCode.WAKE_FAILED,
5862
5872
  });
5863
- return;
5873
+ return fail(MiniappErrorCode.WAKE_FAILED, `${target} disconnected before the action could be delivered`);
5864
5874
  }
5865
- // Deliver the call and arm the handler timeout; ACTION_RESULT resolves it.
5875
+ // Deliver the call and arm the handler timeout; ACTION_RESULT settles it.
5866
5876
  const callId = `act-${Date.now().toString(36)}-${this.actionCallSeq++}`;
5867
- const timer = BgTimer.setTimeout(() => {
5868
- this.actionCalls.delete(callId);
5869
- this.sendResult(callerPackageName, requestId, false, undefined, {
5870
- code: MiniappErrorCode.ACTION_TIMEOUT,
5871
- message: `action "${actionId}" timed out after ${timeoutMs}ms`,
5872
- });
5873
- }, timeoutMs);
5874
- this.actionCalls.set(callId, { callerPackageName, targetPackageName: target, callerRequestId: requestId, timer });
5877
+ const outcome = this.actionCalls.register(callId, target, actionId, timeoutMs);
5875
5878
  this.sendToMiniapp(target, {
5876
5879
  type: MiniappResponseType.ACTION_CALL,
5877
5880
  callId,
@@ -5880,40 +5883,11 @@ class LocalMiniappRuntime {
5880
5883
  callerPackageName,
5881
5884
  });
5882
5885
  this.auditInterop({ caller: callerPackageName, op: "invoke", target, actionId, ok: true });
5886
+ return outcome;
5883
5887
  }
5884
- /** Target → host: forward an ACTION_RESULT back to the caller's pending invoke. */
5888
+ /** Target → host: settle the pending invoke for this ACTION_RESULT. */
5885
5889
  handleActionResult(targetPackageName, payload) {
5886
- const callId = payload.callId;
5887
- if (!callId)
5888
- return;
5889
- const pending = this.actionCalls.get(callId);
5890
- if (!pending)
5891
- return;
5892
- // Only the invoked target may resolve this call. callIds are guessable
5893
- // (timestamp + seq), so a different connected miniapp must not be able to
5894
- // spoof a result/error for someone else's invoke.
5895
- if (pending.targetPackageName !== targetPackageName)
5896
- return;
5897
- this.actionCalls.delete(callId);
5898
- BgTimer.clearTimeout(pending.timer);
5899
- if (payload.ok === true) {
5900
- const result = payload.result;
5901
- if (this.actionPayloadTooLarge(result)) {
5902
- this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, {
5903
- code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
5904
- message: "action result exceeded the 256 KB cap",
5905
- });
5906
- return;
5907
- }
5908
- this.sendResult(pending.callerPackageName, pending.callerRequestId, true, result ?? null);
5909
- }
5910
- else {
5911
- const error = payload.error ?? {
5912
- code: MiniappErrorCode.INTERNAL,
5913
- message: "action handler error",
5914
- };
5915
- this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, error);
5916
- }
5890
+ this.actionCalls.handleResult(targetPackageName, payload);
5917
5891
  }
5918
5892
  sendResult(packageName, requestId, ok, data, error) {
5919
5893
  if (!requestId)