@elizaos/ui 2.0.0-alpha.526 → 2.0.0-alpha.528

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 (237) hide show
  1. package/apps/app-elizamaker/src/index.d.ts +2 -0
  2. package/apps/app-elizamaker/src/index.d.ts.map +1 -1
  3. package/apps/app-elizamaker/src/index.js +2 -0
  4. package/apps/app-elizamaker/src/init-registry-services.d.ts +18 -0
  5. package/apps/app-elizamaker/src/init-registry-services.d.ts.map +1 -0
  6. package/apps/app-elizamaker/src/init-registry-services.js +63 -0
  7. package/apps/app-elizamaker/src/plugin.d.ts.map +1 -1
  8. package/apps/app-elizamaker/src/plugin.js +7 -0
  9. package/apps/app-elizamaker/src/registry-service-registry.d.ts +4 -0
  10. package/apps/app-elizamaker/src/registry-service-registry.d.ts.map +1 -0
  11. package/apps/app-elizamaker/src/registry-service-registry.js +7 -0
  12. package/apps/app-lifeops/src/automation-node-contributor.d.ts +2 -0
  13. package/apps/app-lifeops/src/automation-node-contributor.d.ts.map +1 -0
  14. package/apps/app-lifeops/src/automation-node-contributor.js +93 -0
  15. package/apps/app-lifeops/src/public.d.ts.map +1 -1
  16. package/apps/app-lifeops/src/public.js +2 -0
  17. package/apps/app-training/src/services/training-service-registry.d.ts +4 -0
  18. package/apps/app-training/src/services/training-service-registry.d.ts.map +1 -0
  19. package/apps/app-training/src/services/training-service-registry.js +7 -0
  20. package/package.json +1 -1
  21. package/packages/agent/src/api/agent-status-routes.d.ts +3 -23
  22. package/packages/agent/src/api/agent-status-routes.d.ts.map +1 -1
  23. package/packages/agent/src/api/agent-status-routes.js +7 -40
  24. package/packages/agent/src/api/coordinator-wiring.d.ts.map +1 -0
  25. package/packages/agent/src/api/misc-routes.d.ts.map +1 -1
  26. package/packages/agent/src/api/misc-routes.js +3 -37
  27. package/packages/agent/src/api/nfa-routes.d.ts.map +1 -1
  28. package/packages/agent/src/api/nfa-routes.js +1 -9
  29. package/packages/agent/src/api/provider-switch-routes.d.ts +8 -7
  30. package/packages/agent/src/api/provider-switch-routes.d.ts.map +1 -1
  31. package/packages/agent/src/api/provider-switch-routes.js +41 -26
  32. package/packages/agent/src/api/server-types.d.ts +0 -6
  33. package/packages/agent/src/api/server-types.d.ts.map +1 -1
  34. package/packages/agent/src/api/server.d.ts.map +1 -1
  35. package/packages/agent/src/api/server.js +31 -138
  36. package/packages/agent/src/api/stream-routes.d.ts.map +1 -1
  37. package/packages/agent/src/api/stream-routes.js +6 -8
  38. package/packages/agent/src/api/wallet-trading-profile.d.ts.map +1 -1
  39. package/packages/agent/src/api/wallet-trading-profile.js +2 -23
  40. package/packages/agent/src/auth/account-storage.d.ts.map +1 -1
  41. package/packages/agent/src/auth/account-storage.js +4 -15
  42. package/packages/agent/src/runtime/eliza.d.ts.map +1 -1
  43. package/packages/agent/src/runtime/eliza.js +2 -6
  44. package/packages/agent/src/runtime/first-time-setup.d.ts.map +1 -1
  45. package/packages/agent/src/runtime/first-time-setup.js +1 -7
  46. package/packages/agent/src/runtime/operations/cold-strategy.d.ts +4 -4
  47. package/packages/agent/src/runtime/operations/cold-strategy.d.ts.map +1 -1
  48. package/packages/agent/src/runtime/operations/cold-strategy.js +13 -46
  49. package/packages/agent/src/runtime/operations/health-checks.d.ts +1 -0
  50. package/packages/agent/src/runtime/operations/health-checks.d.ts.map +1 -1
  51. package/packages/agent/src/runtime/operations/health-checks.js +1 -1
  52. package/packages/agent/src/runtime/operations/health.d.ts.map +1 -1
  53. package/packages/agent/src/runtime/operations/health.js +1 -13
  54. package/packages/agent/src/runtime/operations/index.d.ts +1 -0
  55. package/packages/agent/src/runtime/operations/index.d.ts.map +1 -1
  56. package/packages/agent/src/runtime/operations/index.js +1 -0
  57. package/packages/agent/src/runtime/operations/manager.d.ts +2 -0
  58. package/packages/agent/src/runtime/operations/manager.d.ts.map +1 -1
  59. package/packages/agent/src/runtime/operations/manager.js +27 -7
  60. package/packages/agent/src/runtime/operations/reload-hot.d.ts +4 -1
  61. package/packages/agent/src/runtime/operations/reload-hot.d.ts.map +1 -1
  62. package/packages/agent/src/runtime/operations/reload-hot.js +46 -41
  63. package/packages/agent/src/runtime/operations/repository.d.ts +30 -8
  64. package/packages/agent/src/runtime/operations/repository.d.ts.map +1 -1
  65. package/packages/agent/src/runtime/operations/repository.js +140 -54
  66. package/packages/agent/src/runtime/operations/types.d.ts +26 -24
  67. package/packages/agent/src/runtime/operations/types.d.ts.map +1 -1
  68. package/packages/agent/src/runtime/operations/types.js +8 -20
  69. package/packages/agent/src/runtime/operations/vault-bridge.d.ts +60 -0
  70. package/packages/agent/src/runtime/operations/vault-bridge.d.ts.map +1 -0
  71. package/packages/agent/src/runtime/operations/vault-bridge.js +80 -0
  72. package/packages/agent/src/runtime/plugin-resolver.d.ts.map +1 -1
  73. package/packages/agent/src/runtime/plugin-resolver.js +1 -3
  74. package/packages/agent/src/runtime/plugin-types.d.ts.map +1 -1
  75. package/packages/agent/src/runtime/plugin-types.js +1 -3
  76. package/packages/agent/src/services/app-package-modules.d.ts.map +1 -1
  77. package/packages/agent/src/services/app-package-modules.js +1 -8
  78. package/packages/agent/src/services/app-run-store.d.ts.map +1 -1
  79. package/packages/agent/src/services/app-run-store.js +2 -23
  80. package/packages/agent/src/services/registry-client-local.d.ts.map +1 -1
  81. package/packages/agent/src/services/registry-client-local.js +1 -9
  82. package/packages/agent/src/utils/atomic-json.d.ts +37 -0
  83. package/packages/agent/src/utils/atomic-json.d.ts.map +1 -0
  84. package/packages/agent/src/utils/atomic-json.js +98 -0
  85. package/packages/app-core/src/api/automation-node-contributors.d.ts +14 -0
  86. package/packages/app-core/src/api/automation-node-contributors.d.ts.map +1 -0
  87. package/packages/app-core/src/api/automation-node-contributors.js +14 -0
  88. package/packages/app-core/src/api/client-agent.d.ts +627 -0
  89. package/packages/app-core/src/api/client-agent.d.ts.map +1 -0
  90. package/packages/app-core/src/api/client-agent.js +1442 -0
  91. package/packages/app-core/src/api/client-automations.d.ts +8 -0
  92. package/packages/app-core/src/api/client-automations.d.ts.map +1 -0
  93. package/packages/app-core/src/api/client-automations.js +7 -0
  94. package/packages/app-core/src/api/client-base.d.ts +82 -0
  95. package/packages/app-core/src/api/client-base.d.ts.map +1 -0
  96. package/packages/app-core/src/api/client-base.js +729 -0
  97. package/packages/app-core/src/api/client-browser-workspace.d.ts +25 -0
  98. package/packages/app-core/src/api/client-browser-workspace.d.ts.map +1 -0
  99. package/packages/app-core/src/api/client-browser-workspace.js +98 -0
  100. package/packages/app-core/src/api/client-chat.d.ts +355 -0
  101. package/packages/app-core/src/api/client-chat.d.ts.map +1 -0
  102. package/packages/app-core/src/api/client-chat.js +582 -0
  103. package/packages/app-core/src/api/client-cloud.d.ts +286 -0
  104. package/packages/app-core/src/api/client-cloud.d.ts.map +1 -0
  105. package/packages/app-core/src/api/client-cloud.js +410 -0
  106. package/packages/app-core/src/api/client-computeruse.d.ts +32 -0
  107. package/packages/app-core/src/api/client-computeruse.d.ts.map +1 -0
  108. package/packages/app-core/src/api/client-computeruse.js +16 -0
  109. package/packages/app-core/src/api/client-imessage.d.ts +68 -0
  110. package/packages/app-core/src/api/client-imessage.d.ts.map +1 -0
  111. package/packages/app-core/src/api/client-imessage.js +74 -0
  112. package/packages/app-core/src/api/client-local-inference.d.ts +62 -0
  113. package/packages/app-core/src/api/client-local-inference.d.ts.map +1 -0
  114. package/packages/app-core/src/api/client-local-inference.js +87 -0
  115. package/packages/app-core/src/api/client-n8n.d.ts +27 -0
  116. package/packages/app-core/src/api/client-n8n.d.ts.map +1 -0
  117. package/packages/app-core/src/api/client-n8n.js +61 -0
  118. package/packages/app-core/src/api/client-skills.d.ts +475 -0
  119. package/packages/app-core/src/api/client-skills.d.ts.map +1 -0
  120. package/packages/app-core/src/api/client-skills.js +608 -0
  121. package/packages/app-core/src/api/client-types-babylon.d.ts +254 -0
  122. package/packages/app-core/src/api/client-types-babylon.d.ts.map +1 -0
  123. package/packages/app-core/src/api/client-types-babylon.js +2 -0
  124. package/packages/app-core/src/api/client-types-character.d.ts +39 -0
  125. package/packages/app-core/src/api/client-types-character.d.ts.map +1 -0
  126. package/packages/app-core/src/api/client-types-character.js +1 -0
  127. package/packages/app-core/src/api/client-types-chat.d.ts +428 -0
  128. package/packages/app-core/src/api/client-types-chat.d.ts.map +1 -0
  129. package/packages/app-core/src/api/client-types-chat.js +9 -0
  130. package/packages/app-core/src/api/client-types-config.d.ts +587 -0
  131. package/packages/app-core/src/api/client-types-config.d.ts.map +1 -0
  132. package/packages/app-core/src/api/client-types-config.js +5 -0
  133. package/packages/app-core/src/api/client-types-experience.d.ts +97 -0
  134. package/packages/app-core/src/api/client-types-experience.d.ts.map +1 -0
  135. package/packages/app-core/src/api/client-types-experience.js +1 -0
  136. package/packages/app-core/src/api/client-types-relationships.d.ts +181 -0
  137. package/packages/app-core/src/api/client-types-relationships.d.ts.map +1 -0
  138. package/packages/app-core/src/api/client-types-relationships.js +1 -0
  139. package/packages/app-core/src/api/client-types-steward.d.ts +85 -0
  140. package/packages/app-core/src/api/client-types-steward.d.ts.map +1 -0
  141. package/packages/app-core/src/api/client-types-steward.js +9 -0
  142. package/packages/app-core/src/api/client-types.d.ts +9 -0
  143. package/packages/app-core/src/api/client-types.d.ts.map +1 -0
  144. package/packages/app-core/src/api/client-types.js +12 -0
  145. package/packages/app-core/src/api/client-wallet.d.ts +130 -0
  146. package/packages/app-core/src/api/client-wallet.d.ts.map +1 -0
  147. package/packages/app-core/src/api/client-wallet.js +228 -0
  148. package/packages/app-core/src/api/client.d.ts +36 -0
  149. package/packages/app-core/src/api/client.d.ts.map +1 -0
  150. package/packages/app-core/src/api/client.js +36 -0
  151. package/packages/app-core/src/api/index.d.ts +2 -0
  152. package/packages/app-core/src/api/index.d.ts.map +1 -0
  153. package/packages/app-core/src/api/index.js +1 -0
  154. package/packages/app-core/src/bridge/electrobun-rpc.d.ts +57 -0
  155. package/packages/app-core/src/bridge/electrobun-rpc.d.ts.map +1 -0
  156. package/packages/app-core/src/bridge/electrobun-rpc.js +87 -0
  157. package/packages/app-core/src/bridge/electrobun-runtime.d.ts +3 -0
  158. package/packages/app-core/src/bridge/electrobun-runtime.d.ts.map +1 -0
  159. package/packages/app-core/src/bridge/electrobun-runtime.js +34 -0
  160. package/packages/app-core/src/bridge/native-plugins.d.ts +504 -0
  161. package/packages/app-core/src/bridge/native-plugins.d.ts.map +1 -0
  162. package/packages/app-core/src/bridge/native-plugins.js +67 -0
  163. package/packages/app-core/src/chat/coding-agent-session-state.d.ts +16 -0
  164. package/packages/app-core/src/chat/coding-agent-session-state.d.ts.map +1 -0
  165. package/packages/app-core/src/chat/coding-agent-session-state.js +27 -0
  166. package/packages/app-core/src/components/pages/browser-workspace-wallet.d.ts +80 -0
  167. package/packages/app-core/src/components/pages/browser-workspace-wallet.d.ts.map +1 -0
  168. package/packages/app-core/src/components/pages/browser-workspace-wallet.js +135 -0
  169. package/packages/app-core/src/services/account-pool.d.ts +98 -0
  170. package/packages/app-core/src/services/account-pool.d.ts.map +1 -0
  171. package/packages/app-core/src/services/account-pool.js +506 -0
  172. package/packages/app-core/src/services/account-usage.d.ts +75 -0
  173. package/packages/app-core/src/services/account-usage.d.ts.map +1 -0
  174. package/packages/app-core/src/services/account-usage.js +179 -0
  175. package/packages/app-core/src/services/local-inference/active-model.d.ts +59 -0
  176. package/packages/app-core/src/services/local-inference/active-model.d.ts.map +1 -0
  177. package/packages/app-core/src/services/local-inference/active-model.js +120 -0
  178. package/packages/app-core/src/services/local-inference/device-bridge.d.ts +131 -0
  179. package/packages/app-core/src/services/local-inference/device-bridge.d.ts.map +1 -0
  180. package/packages/app-core/src/services/local-inference/device-bridge.js +748 -0
  181. package/packages/app-core/src/services/local-inference/engine.d.ts +52 -0
  182. package/packages/app-core/src/services/local-inference/engine.d.ts.map +1 -0
  183. package/packages/app-core/src/services/local-inference/engine.js +138 -0
  184. package/packages/app-core/src/services/local-inference/external-scanner.d.ts +17 -0
  185. package/packages/app-core/src/services/local-inference/external-scanner.d.ts.map +1 -0
  186. package/packages/app-core/src/services/local-inference/external-scanner.js +261 -0
  187. package/packages/app-core/src/services/local-inference/handler-registry.d.ts +72 -0
  188. package/packages/app-core/src/services/local-inference/handler-registry.d.ts.map +1 -0
  189. package/packages/app-core/src/services/local-inference/handler-registry.js +160 -0
  190. package/packages/app-core/src/services/local-inference/paths.d.ts +24 -0
  191. package/packages/app-core/src/services/local-inference/paths.d.ts.map +1 -0
  192. package/packages/app-core/src/services/local-inference/paths.js +41 -0
  193. package/packages/app-core/src/services/local-inference/providers.d.ts +61 -0
  194. package/packages/app-core/src/services/local-inference/providers.d.ts.map +1 -0
  195. package/packages/app-core/src/services/local-inference/providers.js +277 -0
  196. package/packages/app-core/src/services/local-inference/registry.d.ts +35 -0
  197. package/packages/app-core/src/services/local-inference/registry.d.ts.map +1 -0
  198. package/packages/app-core/src/services/local-inference/registry.js +108 -0
  199. package/packages/app-core/src/services/local-inference/routing-preferences.d.ts +31 -0
  200. package/packages/app-core/src/services/local-inference/routing-preferences.d.ts.map +1 -0
  201. package/packages/app-core/src/services/local-inference/routing-preferences.js +72 -0
  202. package/packages/app-core/src/services/local-inference/types.d.ts +121 -0
  203. package/packages/app-core/src/services/local-inference/types.d.ts.map +1 -0
  204. package/packages/app-core/src/services/local-inference/types.js +14 -0
  205. package/packages/app-core/src/services/local-inference/verify.d.ts +39 -0
  206. package/packages/app-core/src/services/local-inference/verify.d.ts.map +1 -0
  207. package/packages/app-core/src/services/local-inference/verify.js +104 -0
  208. package/packages/app-core/src/types/index.d.ts +658 -0
  209. package/packages/app-core/src/types/index.d.ts.map +1 -0
  210. package/packages/app-core/src/types/index.js +4 -0
  211. package/packages/app-core/src/utils/assistant-text.d.ts +2 -0
  212. package/packages/app-core/src/utils/assistant-text.d.ts.map +1 -0
  213. package/packages/app-core/src/utils/assistant-text.js +167 -0
  214. package/packages/app-core/src/utils/eliza-globals.d.ts +15 -0
  215. package/packages/app-core/src/utils/eliza-globals.d.ts.map +1 -0
  216. package/packages/app-core/src/utils/eliza-globals.js +50 -0
  217. package/packages/app-core/src/utils/streaming-text.d.ts +9 -0
  218. package/packages/app-core/src/utils/streaming-text.d.ts.map +1 -0
  219. package/packages/app-core/src/utils/streaming-text.js +112 -0
  220. package/packages/shared/src/format-error.d.ts +14 -0
  221. package/packages/shared/src/format-error.d.ts.map +1 -0
  222. package/packages/shared/src/format-error.js +17 -0
  223. package/packages/shared/src/index.d.ts +1 -0
  224. package/packages/shared/src/index.d.ts.map +1 -1
  225. package/packages/shared/src/index.js +1 -0
  226. package/packages/typescript/src/runtime.d.ts +14 -0
  227. package/packages/typescript/src/runtime.d.ts.map +1 -1
  228. package/packages/typescript/src/runtime.js +42 -0
  229. package/packages/typescript/src/services/message.d.ts +8 -0
  230. package/packages/typescript/src/services/message.d.ts.map +1 -1
  231. package/packages/typescript/src/services/message.js +51 -0
  232. package/packages/typescript/src/services/task.d.ts +1 -0
  233. package/packages/typescript/src/services/task.d.ts.map +1 -1
  234. package/packages/typescript/src/services/task.js +26 -3
  235. package/apps/app-task-coordinator/src/api/coordinator-wiring.d.ts.map +0 -1
  236. /package/{apps/app-task-coordinator → packages/agent}/src/api/coordinator-wiring.d.ts +0 -0
  237. /package/{apps/app-task-coordinator → packages/agent}/src/api/coordinator-wiring.js +0 -0
@@ -0,0 +1,748 @@
1
+ /**
2
+ * Device-bridge: agent-side half of the "inference on the user's phone,
3
+ * agent in a container" architecture.
4
+ *
5
+ * Multi-device aware. Any number of devices can dial in; each `generate`
6
+ * is routed to the highest-scoring connected device at call time. A phone
7
+ * and a Mac paired to the same agent → requests go to the Mac; when the
8
+ * Mac disconnects, new requests fall through to the phone automatically.
9
+ *
10
+ * Scoring (higher = preferred):
11
+ * - desktop / electrobun: 100 base
12
+ * - ios / android: 10 base
13
+ * - per GB of total RAM: +2
14
+ * - per GB of VRAM: +5 (dedicated GPU wins big)
15
+ * - has loaded the right model already: +50 (avoid a swap)
16
+ *
17
+ * Disconnect tolerance
18
+ * --------------------
19
+ * A pending request stays in `pendingGenerates` until either (a) a device
20
+ * (same or different) returns a matching correlation-id, or (b) the
21
+ * timeout fires. On any device (re)connect we re-route orphaned
22
+ * generates to the new best device.
23
+ *
24
+ * Durability
25
+ * ----------
26
+ * Pending requests are best-effort persisted to a JSON log under
27
+ * `$ELIZA_STATE_DIR/local-inference/pending-requests.json` so a brief
28
+ * agent restart doesn't lose the queue. Persistence is async and
29
+ * non-blocking — failures fall back to in-memory only.
30
+ */
31
+ import { randomUUID } from "node:crypto";
32
+ import fs from "node:fs/promises";
33
+ import path from "node:path";
34
+ import { logger } from "@elizaos/core";
35
+ import { localInferenceRoot } from "./paths";
36
+ const DEFAULT_CALL_TIMEOUT_MS = 60_000;
37
+ const DEFAULT_LOAD_TIMEOUT_MS = 120_000;
38
+ const HEARTBEAT_INTERVAL_MS = 15_000;
39
+ const PENDING_LOG_FILENAME = "pending-requests.json";
40
+ /**
41
+ * Scoring function — pick the most powerful device available.
42
+ * Pure, synchronous, and easy to test.
43
+ */
44
+ function scoreDevice(device, opts = {}) {
45
+ const cap = device.capabilities;
46
+ const platformBase = cap.platform === "desktop" || cap.platform === "electrobun"
47
+ ? 100
48
+ : cap.platform === "ios" || cap.platform === "android"
49
+ ? 10
50
+ : 0;
51
+ const ramScore = cap.totalRamGb * 2;
52
+ const vramScore = cap.gpu?.available
53
+ ? (cap.gpu.totalVramGb ?? cap.totalRamGb) * 5
54
+ : 0;
55
+ const loadedBonus = opts.preferLoadedPath && device.loadedPath === opts.preferLoadedPath
56
+ ? 50
57
+ : 0;
58
+ return platformBase + ramScore + vramScore + loadedBonus;
59
+ }
60
+ export class DeviceBridge {
61
+ devices = new Map();
62
+ wss = null;
63
+ restored = false;
64
+ pendingLoads = new Map();
65
+ pendingUnloads = new Map();
66
+ pendingGenerates = new Map();
67
+ pendingEmbeds = new Map();
68
+ statusListeners = new Set();
69
+ expectedPairingToken = process.env.ELIZA_DEVICE_PAIRING_TOKEN?.trim() || null;
70
+ status() {
71
+ const summaries = [];
72
+ for (const device of this.devices.values()) {
73
+ const score = scoreDevice(device);
74
+ const activeRequests = this.countRouted(this.pendingGenerates, device.deviceId) +
75
+ this.countRouted(this.pendingEmbeds, device.deviceId) +
76
+ this.countRouted(this.pendingLoads, device.deviceId) +
77
+ this.countRouted(this.pendingUnloads, device.deviceId);
78
+ summaries.push({
79
+ deviceId: device.deviceId,
80
+ capabilities: device.capabilities,
81
+ loadedPath: device.loadedPath,
82
+ connectedSince: new Date(device.connectedAt).toISOString(),
83
+ score,
84
+ activeRequests,
85
+ isPrimary: false,
86
+ });
87
+ }
88
+ // Sort desc by score so the UI can just render in order.
89
+ summaries.sort((a, b) => b.score - a.score);
90
+ if (summaries[0])
91
+ summaries[0].isPrimary = true;
92
+ const primary = summaries[0] ?? null;
93
+ const pendingRequests = this.pendingGenerates.size +
94
+ this.pendingEmbeds.size +
95
+ this.pendingLoads.size +
96
+ this.pendingUnloads.size;
97
+ return {
98
+ connected: summaries.length > 0,
99
+ devices: summaries,
100
+ primaryDeviceId: primary?.deviceId ?? null,
101
+ pendingRequests,
102
+ deviceId: primary?.deviceId ?? null,
103
+ capabilities: primary?.capabilities ?? null,
104
+ loadedPath: primary?.loadedPath ?? null,
105
+ connectedSince: primary?.connectedSince ?? null,
106
+ };
107
+ }
108
+ countRouted(map, deviceId) {
109
+ let n = 0;
110
+ for (const value of map.values()) {
111
+ if (value.routedDeviceId === deviceId)
112
+ n += 1;
113
+ }
114
+ return n;
115
+ }
116
+ subscribeStatus(listener) {
117
+ this.statusListeners.add(listener);
118
+ return () => {
119
+ this.statusListeners.delete(listener);
120
+ };
121
+ }
122
+ emitStatus() {
123
+ const snapshot = this.status();
124
+ for (const listener of this.statusListeners) {
125
+ try {
126
+ listener(snapshot);
127
+ }
128
+ catch {
129
+ this.statusListeners.delete(listener);
130
+ }
131
+ }
132
+ }
133
+ async attachToHttpServer(server) {
134
+ if (this.wss)
135
+ return;
136
+ const ws = (await import("ws"));
137
+ const wss = new ws.WebSocketServer({
138
+ noServer: true,
139
+ maxPayload: 1024 * 1024,
140
+ });
141
+ this.wss = wss;
142
+ wss.on("error", (err) => {
143
+ logger.warn("[device-bridge] WSS error:", err.message);
144
+ });
145
+ server.on("upgrade", (request, socket, head) => {
146
+ const url = new URL(request.url ?? "/", "http://localhost");
147
+ if (url.pathname !== "/api/local-inference/device-bridge")
148
+ return;
149
+ wss.handleUpgrade(request, socket, head, (client) => {
150
+ this.handleConnection(client, ws.WebSocket, url);
151
+ });
152
+ });
153
+ // Restore persisted pending generates the first time a server attaches.
154
+ // We only restore once per process — avoids double-resubmit on repeated
155
+ // server restarts inside the same worker.
156
+ if (!this.restored) {
157
+ this.restored = true;
158
+ await this.restorePendingGenerates();
159
+ }
160
+ }
161
+ handleConnection(socket, WsCtor, url) {
162
+ const queryToken = url.searchParams.get("token")?.trim();
163
+ if (this.expectedPairingToken && queryToken !== this.expectedPairingToken) {
164
+ logger.warn("[device-bridge] Rejecting connection: bad query token");
165
+ socket.close(4001, "unauthorized");
166
+ return;
167
+ }
168
+ let registered = false;
169
+ let registeredDeviceId = null;
170
+ socket.on("message", (raw) => {
171
+ let msg;
172
+ try {
173
+ const text = typeof raw === "string" ? raw : raw.toString("utf8");
174
+ msg = JSON.parse(text);
175
+ }
176
+ catch {
177
+ logger.warn("[device-bridge] Ignoring non-JSON frame");
178
+ return;
179
+ }
180
+ if (!registered) {
181
+ if (msg.type !== "register") {
182
+ logger.warn("[device-bridge] First frame must be register");
183
+ socket.close(4002, "must-register-first");
184
+ return;
185
+ }
186
+ if (this.expectedPairingToken &&
187
+ msg.payload.pairingToken !== this.expectedPairingToken) {
188
+ logger.warn("[device-bridge] Rejecting register: bad pairing token");
189
+ socket.close(4001, "unauthorized");
190
+ return;
191
+ }
192
+ registered = true;
193
+ registeredDeviceId = msg.payload.deviceId;
194
+ this.onDeviceRegistered(socket, WsCtor, msg.payload);
195
+ return;
196
+ }
197
+ this.handleDeviceMessage(msg);
198
+ });
199
+ socket.on("close", () => {
200
+ if (!registered || !registeredDeviceId)
201
+ return;
202
+ // Only evict if THIS socket is still the current one for the
203
+ // deviceId. When a newer connection supersedes us, its registration
204
+ // already replaced the map entry; the delayed close event from our
205
+ // superseded socket must not tear that down.
206
+ const current = this.devices.get(registeredDeviceId);
207
+ if (current && current.socket === socket) {
208
+ this.onDeviceDisconnected(registeredDeviceId);
209
+ }
210
+ });
211
+ socket.on("error", (err) => {
212
+ logger.warn("[device-bridge] Socket error:", err.message);
213
+ });
214
+ }
215
+ onDeviceRegistered(socket, WsCtor, registration) {
216
+ // Supersede any existing connection under the same deviceId.
217
+ const existing = this.devices.get(registration.deviceId);
218
+ if (existing) {
219
+ try {
220
+ existing.socket.close(4003, "superseded");
221
+ }
222
+ catch {
223
+ /* best effort */
224
+ }
225
+ clearInterval(existing.heartbeatTimer);
226
+ }
227
+ const device = {
228
+ deviceId: registration.deviceId,
229
+ socket,
230
+ capabilities: registration.capabilities,
231
+ loadedPath: registration.loadedPath,
232
+ connectedAt: Date.now(),
233
+ lastHeartbeatAt: Date.now(),
234
+ heartbeatTimer: setInterval(() => {
235
+ if (socket.readyState !== WsCtor.OPEN)
236
+ return;
237
+ try {
238
+ this.sendToDevice(device.deviceId, { type: "ping", at: Date.now() });
239
+ }
240
+ catch {
241
+ /* ignore after close */
242
+ }
243
+ }, HEARTBEAT_INTERVAL_MS),
244
+ };
245
+ if (typeof device.heartbeatTimer === "object" &&
246
+ device.heartbeatTimer &&
247
+ "unref" in device.heartbeatTimer) {
248
+ device.heartbeatTimer.unref();
249
+ }
250
+ this.devices.set(device.deviceId, device);
251
+ logger.info(`[device-bridge] Device connected: ${device.deviceId} (${device.capabilities.platform}, score=${scoreDevice(device)})`);
252
+ // Re-route any orphaned generates (the ones whose prior routed device
253
+ // disconnected). Load/unload orphans reject — device-specific state.
254
+ for (const pending of this.pendingLoads.values()) {
255
+ if (pending.routedDeviceId === device.deviceId)
256
+ continue;
257
+ if (!this.devices.has(pending.routedDeviceId)) {
258
+ clearTimeout(pending.timeout);
259
+ this.pendingLoads.delete(pending.correlationId);
260
+ pending.reject(new Error("DEVICE_RECONNECTED: retry model load after reconnect"));
261
+ }
262
+ }
263
+ for (const pending of this.pendingUnloads.values()) {
264
+ if (!this.devices.has(pending.routedDeviceId)) {
265
+ clearTimeout(pending.timeout);
266
+ this.pendingUnloads.delete(pending.correlationId);
267
+ pending.reject(new Error("DEVICE_RECONNECTED: retry model unload after reconnect"));
268
+ }
269
+ }
270
+ for (const pending of this.pendingGenerates.values()) {
271
+ if (pending.routedDeviceId === null) {
272
+ const best = this.pickBestDevice();
273
+ if (best) {
274
+ pending.routedDeviceId = best.deviceId;
275
+ try {
276
+ this.sendToDevice(best.deviceId, pending.request);
277
+ }
278
+ catch (err) {
279
+ pending.reject(err instanceof Error
280
+ ? err
281
+ : new Error("Failed to re-route after reconnect"));
282
+ }
283
+ }
284
+ }
285
+ }
286
+ // Same re-route logic for orphaned embeds. Embeds are short-lived and
287
+ // idempotent (the device just runs llama_get_embeddings), so we can
288
+ // safely retarget them on reconnect.
289
+ for (const pending of this.pendingEmbeds.values()) {
290
+ if (pending.routedDeviceId === null) {
291
+ const best = this.pickBestDevice();
292
+ if (best) {
293
+ pending.routedDeviceId = best.deviceId;
294
+ try {
295
+ this.sendToDevice(best.deviceId, pending.request);
296
+ }
297
+ catch (err) {
298
+ pending.reject(err instanceof Error
299
+ ? err
300
+ : new Error("Failed to re-route after reconnect"));
301
+ }
302
+ }
303
+ }
304
+ }
305
+ this.emitStatus();
306
+ }
307
+ onDeviceDisconnected(deviceId) {
308
+ const device = this.devices.get(deviceId);
309
+ if (!device)
310
+ return;
311
+ clearInterval(device.heartbeatTimer);
312
+ this.devices.delete(deviceId);
313
+ // Orphan any generates / embeds routed to this device so they can be
314
+ // re-routed to a surviving device (or await a reconnect).
315
+ let orphaned = 0;
316
+ for (const pending of this.pendingGenerates.values()) {
317
+ if (pending.routedDeviceId === deviceId) {
318
+ pending.routedDeviceId = null;
319
+ orphaned += 1;
320
+ }
321
+ }
322
+ for (const pending of this.pendingEmbeds.values()) {
323
+ if (pending.routedDeviceId === deviceId) {
324
+ pending.routedDeviceId = null;
325
+ orphaned += 1;
326
+ }
327
+ }
328
+ logger.info(`[device-bridge] Device disconnected: ${deviceId}; ${orphaned} request(s) orphaned`);
329
+ // Fast-path: if there are other connected devices, re-route now.
330
+ if (this.devices.size > 0) {
331
+ for (const pending of this.pendingGenerates.values()) {
332
+ if (pending.routedDeviceId === null) {
333
+ const best = this.pickBestDevice();
334
+ if (best) {
335
+ pending.routedDeviceId = best.deviceId;
336
+ try {
337
+ this.sendToDevice(best.deviceId, pending.request);
338
+ }
339
+ catch {
340
+ /* will be retried on the next reconnect */
341
+ }
342
+ }
343
+ }
344
+ }
345
+ for (const pending of this.pendingEmbeds.values()) {
346
+ if (pending.routedDeviceId === null) {
347
+ const best = this.pickBestDevice();
348
+ if (best) {
349
+ pending.routedDeviceId = best.deviceId;
350
+ try {
351
+ this.sendToDevice(best.deviceId, pending.request);
352
+ }
353
+ catch {
354
+ /* will be retried on the next reconnect */
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ this.emitStatus();
361
+ }
362
+ handleDeviceMessage(msg) {
363
+ if (msg.type === "pong") {
364
+ // Heartbeat round-trip — could update lastHeartbeatAt per device, but
365
+ // we don't currently use it for eviction.
366
+ return;
367
+ }
368
+ if (msg.type === "loadResult") {
369
+ const pending = this.pendingLoads.get(msg.correlationId);
370
+ if (!pending)
371
+ return;
372
+ clearTimeout(pending.timeout);
373
+ this.pendingLoads.delete(msg.correlationId);
374
+ if (msg.ok === false) {
375
+ pending.reject(new Error(msg.error));
376
+ }
377
+ else {
378
+ const device = this.devices.get(pending.routedDeviceId);
379
+ if (device)
380
+ device.loadedPath = msg.loadedPath;
381
+ pending.resolve();
382
+ this.emitStatus();
383
+ }
384
+ return;
385
+ }
386
+ if (msg.type === "unloadResult") {
387
+ const pending = this.pendingUnloads.get(msg.correlationId);
388
+ if (!pending)
389
+ return;
390
+ clearTimeout(pending.timeout);
391
+ this.pendingUnloads.delete(msg.correlationId);
392
+ if (msg.ok === false) {
393
+ pending.reject(new Error(msg.error));
394
+ }
395
+ else {
396
+ const device = this.devices.get(pending.routedDeviceId);
397
+ if (device)
398
+ device.loadedPath = null;
399
+ pending.resolve();
400
+ this.emitStatus();
401
+ }
402
+ return;
403
+ }
404
+ if (msg.type === "generateResult") {
405
+ const pending = this.pendingGenerates.get(msg.correlationId);
406
+ if (!pending)
407
+ return;
408
+ clearTimeout(pending.timeout);
409
+ this.pendingGenerates.delete(msg.correlationId);
410
+ // Best-effort purge the persisted copy.
411
+ void this.persistPendingGenerates();
412
+ if (msg.ok === false) {
413
+ pending.reject(new Error(msg.error));
414
+ }
415
+ else {
416
+ pending.resolve(msg.text);
417
+ }
418
+ return;
419
+ }
420
+ if (msg.type === "embedResult") {
421
+ const pending = this.pendingEmbeds.get(msg.correlationId);
422
+ if (!pending)
423
+ return;
424
+ clearTimeout(pending.timeout);
425
+ this.pendingEmbeds.delete(msg.correlationId);
426
+ if (msg.ok === false) {
427
+ pending.reject(new Error(msg.error));
428
+ }
429
+ else {
430
+ pending.resolve({ embedding: msg.embedding, tokens: msg.tokens });
431
+ }
432
+ return;
433
+ }
434
+ }
435
+ sendToDevice(deviceId, msg) {
436
+ const device = this.devices.get(deviceId);
437
+ if (!device)
438
+ throw new Error(`DEVICE_DISCONNECTED: ${deviceId}`);
439
+ device.socket.send(JSON.stringify(msg));
440
+ }
441
+ /** Highest-scoring connected device, optionally boosted for an already-loaded model. */
442
+ pickBestDevice(opts) {
443
+ let best = null;
444
+ let bestScore = -Infinity;
445
+ for (const device of this.devices.values()) {
446
+ const score = scoreDevice(device, opts);
447
+ if (score > bestScore) {
448
+ best = device;
449
+ bestScore = score;
450
+ }
451
+ }
452
+ return best;
453
+ }
454
+ // ── LocalInferenceLoader surface ──────────────────────────────────────
455
+ async loadModel(args) {
456
+ const best = this.pickBestDevice({ preferLoadedPath: args.modelPath });
457
+ if (!best) {
458
+ throw new Error("DEVICE_DISCONNECTED: no mobile / desktop bridge device attached");
459
+ }
460
+ const correlationId = randomUUID();
461
+ return new Promise((resolve, reject) => {
462
+ const timeout = setTimeout(() => {
463
+ this.pendingLoads.delete(correlationId);
464
+ reject(new Error("DEVICE_TIMEOUT: model load exceeded deadline"));
465
+ }, DEFAULT_LOAD_TIMEOUT_MS);
466
+ if (typeof timeout === "object" && timeout && "unref" in timeout) {
467
+ timeout.unref();
468
+ }
469
+ this.pendingLoads.set(correlationId, {
470
+ correlationId,
471
+ modelPath: args.modelPath,
472
+ resolve,
473
+ reject,
474
+ timeout,
475
+ routedDeviceId: best.deviceId,
476
+ });
477
+ try {
478
+ this.sendToDevice(best.deviceId, {
479
+ type: "load",
480
+ correlationId,
481
+ modelPath: args.modelPath,
482
+ contextSize: args.contextSize,
483
+ useGpu: args.useGpu,
484
+ });
485
+ }
486
+ catch (err) {
487
+ clearTimeout(timeout);
488
+ this.pendingLoads.delete(correlationId);
489
+ reject(err instanceof Error ? err : new Error(String(err)));
490
+ }
491
+ });
492
+ }
493
+ async unloadModel() {
494
+ // Unload every device that currently has a model loaded. Best-effort —
495
+ // individual failures don't block the others.
496
+ const targets = [...this.devices.values()].filter((d) => d.loadedPath);
497
+ if (targets.length === 0)
498
+ return;
499
+ await Promise.allSettled(targets.map((device) => new Promise((resolve, reject) => {
500
+ const correlationId = randomUUID();
501
+ const timeout = setTimeout(() => {
502
+ this.pendingUnloads.delete(correlationId);
503
+ reject(new Error("DEVICE_TIMEOUT: unload exceeded deadline"));
504
+ }, DEFAULT_CALL_TIMEOUT_MS);
505
+ if (typeof timeout === "object" && timeout && "unref" in timeout) {
506
+ timeout.unref();
507
+ }
508
+ this.pendingUnloads.set(correlationId, {
509
+ correlationId,
510
+ resolve,
511
+ reject,
512
+ timeout,
513
+ routedDeviceId: device.deviceId,
514
+ });
515
+ try {
516
+ this.sendToDevice(device.deviceId, {
517
+ type: "unload",
518
+ correlationId,
519
+ });
520
+ }
521
+ catch (err) {
522
+ clearTimeout(timeout);
523
+ this.pendingUnloads.delete(correlationId);
524
+ reject(err instanceof Error ? err : new Error(String(err)));
525
+ }
526
+ })));
527
+ }
528
+ currentModelPath() {
529
+ // The primary device's loaded path wins — consistent with which device
530
+ // would actually run the next generate.
531
+ const best = this.pickBestDevice();
532
+ return best?.loadedPath ?? null;
533
+ }
534
+ async embed(args) {
535
+ const envTimeout = Number.parseInt(process.env.ELIZA_DEVICE_GENERATE_TIMEOUT_MS?.trim() ?? "", 10);
536
+ const timeoutMs = Number.isFinite(envTimeout) && envTimeout > 0
537
+ ? envTimeout
538
+ : DEFAULT_CALL_TIMEOUT_MS;
539
+ const correlationId = randomUUID();
540
+ const request = {
541
+ type: "embed",
542
+ correlationId,
543
+ input: args.input,
544
+ };
545
+ const best = this.pickBestDevice();
546
+ return new Promise((resolve, reject) => {
547
+ const timeout = setTimeout(() => {
548
+ this.pendingEmbeds.delete(correlationId);
549
+ reject(new Error(`DEVICE_TIMEOUT: no device responded to embed within ${timeoutMs}ms`));
550
+ }, timeoutMs);
551
+ if (typeof timeout === "object" && timeout && "unref" in timeout) {
552
+ timeout.unref();
553
+ }
554
+ const pending = {
555
+ correlationId,
556
+ resolve,
557
+ reject,
558
+ timeout,
559
+ request,
560
+ routedDeviceId: best?.deviceId ?? null,
561
+ submittedAt: new Date().toISOString(),
562
+ };
563
+ this.pendingEmbeds.set(correlationId, pending);
564
+ if (best) {
565
+ try {
566
+ this.sendToDevice(best.deviceId, request);
567
+ }
568
+ catch {
569
+ // Routed device went away between pickBestDevice and send.
570
+ // Mark as orphaned; reroute logic will pick it up on the next
571
+ // device (re)connect.
572
+ pending.routedDeviceId = null;
573
+ }
574
+ }
575
+ else {
576
+ logger.debug(`[device-bridge] No device available; parking embed ${correlationId} pending connection`);
577
+ }
578
+ });
579
+ }
580
+ async generate(args) {
581
+ const envTimeout = Number.parseInt(process.env.ELIZA_DEVICE_GENERATE_TIMEOUT_MS?.trim() ?? "", 10);
582
+ const timeoutMs = Number.isFinite(envTimeout) && envTimeout > 0
583
+ ? envTimeout
584
+ : DEFAULT_CALL_TIMEOUT_MS;
585
+ const correlationId = randomUUID();
586
+ const request = {
587
+ type: "generate",
588
+ correlationId,
589
+ prompt: args.prompt,
590
+ stopSequences: args.stopSequences,
591
+ maxTokens: args.maxTokens,
592
+ temperature: args.temperature,
593
+ };
594
+ const best = this.pickBestDevice();
595
+ return new Promise((resolve, reject) => {
596
+ const timeout = setTimeout(() => {
597
+ this.pendingGenerates.delete(correlationId);
598
+ void this.persistPendingGenerates();
599
+ reject(new Error(`DEVICE_TIMEOUT: no device responded within ${timeoutMs}ms`));
600
+ }, timeoutMs);
601
+ if (typeof timeout === "object" && timeout && "unref" in timeout) {
602
+ timeout.unref();
603
+ }
604
+ const pending = {
605
+ correlationId,
606
+ resolve,
607
+ reject,
608
+ timeout,
609
+ request,
610
+ routedDeviceId: best?.deviceId ?? null,
611
+ submittedAt: new Date().toISOString(),
612
+ };
613
+ this.pendingGenerates.set(correlationId, pending);
614
+ void this.persistPendingGenerates();
615
+ if (best) {
616
+ try {
617
+ this.sendToDevice(best.deviceId, request);
618
+ }
619
+ catch {
620
+ pending.routedDeviceId = null;
621
+ }
622
+ }
623
+ else {
624
+ logger.debug(`[device-bridge] No device available; parking generate ${correlationId} pending connection`);
625
+ }
626
+ });
627
+ }
628
+ // ── Durability ────────────────────────────────────────────────────────
629
+ pendingLogPath() {
630
+ return path.join(localInferenceRoot(), PENDING_LOG_FILENAME);
631
+ }
632
+ /**
633
+ * Rewrite the pending-generate log. Called after every mutation to the
634
+ * pendingGenerates map. We only persist `generate` — loads/unloads are
635
+ * bound to a specific device's current state and aren't safely replayable
636
+ * across restart.
637
+ */
638
+ async persistPendingGenerates() {
639
+ try {
640
+ await fs.mkdir(localInferenceRoot(), { recursive: true });
641
+ const payload = [
642
+ ...this.pendingGenerates.values(),
643
+ ].map((p) => ({
644
+ correlationId: p.correlationId,
645
+ request: p.request,
646
+ submittedAt: p.submittedAt,
647
+ }));
648
+ const tmp = `${this.pendingLogPath()}.tmp`;
649
+ await fs.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
650
+ await fs.rename(tmp, this.pendingLogPath());
651
+ }
652
+ catch (err) {
653
+ logger.debug("[device-bridge] Failed to persist pending generates:", err instanceof Error ? err.message : String(err));
654
+ }
655
+ }
656
+ /**
657
+ * On startup, read persisted pending requests back into memory. Their
658
+ * promises are gone (the original caller's process is dead) so they can
659
+ * only be resolved externally — for now we just re-queue them with a
660
+ * fresh timeout, and the first device that connects will process them.
661
+ * If nothing consumes them within the timeout they reject quietly.
662
+ *
663
+ * Stale entries older than 24h are purged rather than resurrected.
664
+ */
665
+ async restorePendingGenerates() {
666
+ let raw;
667
+ try {
668
+ raw = await fs.readFile(this.pendingLogPath(), "utf8");
669
+ }
670
+ catch {
671
+ return;
672
+ }
673
+ let items;
674
+ try {
675
+ items = JSON.parse(raw);
676
+ if (!Array.isArray(items))
677
+ return;
678
+ }
679
+ catch {
680
+ return;
681
+ }
682
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
683
+ let restored = 0;
684
+ for (const item of items) {
685
+ if (!item.correlationId ||
686
+ !item.request ||
687
+ item.request.type !== "generate") {
688
+ continue;
689
+ }
690
+ const submittedAt = Date.parse(item.submittedAt);
691
+ if (!Number.isFinite(submittedAt) || submittedAt < cutoff)
692
+ continue;
693
+ if (this.pendingGenerates.has(item.correlationId))
694
+ continue;
695
+ // The original caller's promise is gone. Queue the request so the
696
+ // first connecting device processes it; if nobody picks it up within
697
+ // the default timeout, drop it.
698
+ const timeout = setTimeout(() => {
699
+ this.pendingGenerates.delete(item.correlationId);
700
+ void this.persistPendingGenerates();
701
+ }, DEFAULT_CALL_TIMEOUT_MS);
702
+ if (typeof timeout === "object" && timeout && "unref" in timeout) {
703
+ timeout.unref();
704
+ }
705
+ this.pendingGenerates.set(item.correlationId, {
706
+ correlationId: item.correlationId,
707
+ request: item.request,
708
+ submittedAt: item.submittedAt,
709
+ routedDeviceId: null,
710
+ timeout,
711
+ resolve: () => {
712
+ /* no caller to resolve */
713
+ },
714
+ reject: () => {
715
+ /* no caller to reject */
716
+ },
717
+ });
718
+ restored += 1;
719
+ }
720
+ if (restored > 0) {
721
+ logger.info(`[device-bridge] Restored ${restored} pending generate(s) from persistent log`);
722
+ }
723
+ }
724
+ }
725
+ export const deviceBridge = new DeviceBridge();
726
+ export function registerDeviceBridgeLoader(runtime) {
727
+ if (typeof runtime.registerService !== "function")
728
+ return;
729
+ const loader = {
730
+ async loadModel(args) {
731
+ await deviceBridge.loadModel(args);
732
+ },
733
+ async unloadModel() {
734
+ await deviceBridge.unloadModel();
735
+ },
736
+ currentModelPath() {
737
+ return deviceBridge.currentModelPath();
738
+ },
739
+ async generate(args) {
740
+ return deviceBridge.generate(args);
741
+ },
742
+ async embed(args) {
743
+ return deviceBridge.embed(args);
744
+ },
745
+ };
746
+ runtime.registerService("localInferenceLoader", loader);
747
+ logger.info("[device-bridge] Registered device-bridge loader for remote on-device inference");
748
+ }