@clinebot/core 0.0.34 → 0.0.36

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 (546) hide show
  1. package/README.md +13 -10
  2. package/dist/ClineCore.d.ts +96 -63
  3. package/dist/ClineCore.d.ts.map +1 -1
  4. package/dist/account/index.d.ts +1 -1
  5. package/dist/account/index.d.ts.map +1 -1
  6. package/dist/account/rpc.d.ts +6 -6
  7. package/dist/account/rpc.d.ts.map +1 -1
  8. package/dist/cron/index.d.ts +6 -0
  9. package/dist/cron/index.d.ts.map +1 -0
  10. package/dist/cron/resource-limiter.d.ts +9 -0
  11. package/dist/cron/resource-limiter.d.ts.map +1 -0
  12. package/dist/cron/schedule-command-service.d.ts +10 -0
  13. package/dist/cron/schedule-command-service.d.ts.map +1 -0
  14. package/dist/cron/schedule-service.d.ts +100 -0
  15. package/dist/cron/schedule-service.d.ts.map +1 -0
  16. package/dist/cron/scheduler.d.ts +66 -0
  17. package/dist/cron/scheduler.d.ts.map +1 -0
  18. package/dist/cron/sqlite-schedule-store.d.ts +52 -0
  19. package/dist/cron/sqlite-schedule-store.d.ts.map +1 -0
  20. package/dist/extensions/config/agent-config-loader.d.ts +6 -5
  21. package/dist/extensions/config/agent-config-loader.d.ts.map +1 -1
  22. package/dist/extensions/config/agent-config-parser.d.ts +1 -1
  23. package/dist/extensions/config/agent-config-parser.d.ts.map +1 -1
  24. package/dist/extensions/config/hooks-config-loader.d.ts +2 -2
  25. package/dist/extensions/config/hooks-config-loader.d.ts.map +1 -1
  26. package/dist/extensions/config/index.d.ts +3 -3
  27. package/dist/extensions/config/index.d.ts.map +1 -1
  28. package/dist/extensions/config/runtime-commands.d.ts +1 -0
  29. package/dist/extensions/config/runtime-commands.d.ts.map +1 -1
  30. package/dist/extensions/config/user-instruction-config-loader.d.ts +3 -2
  31. package/dist/extensions/config/user-instruction-config-loader.d.ts.map +1 -1
  32. package/dist/extensions/context/agentic-compaction.d.ts +2 -2
  33. package/dist/extensions/context/agentic-compaction.d.ts.map +1 -1
  34. package/dist/extensions/context/compaction-shared.d.ts +5 -4
  35. package/dist/extensions/context/compaction-shared.d.ts.map +1 -1
  36. package/dist/extensions/context/compaction.d.ts.map +1 -1
  37. package/dist/extensions/plugin/plugin-config-loader.d.ts +9 -2
  38. package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
  39. package/dist/extensions/plugin/plugin-loader.d.ts +5 -3
  40. package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
  41. package/dist/extensions/plugin/plugin-module-import.d.ts.map +1 -1
  42. package/dist/extensions/plugin/plugin-sandbox.d.ts +15 -2
  43. package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
  44. package/dist/extensions/plugin/plugin-targeting.d.ts +7 -0
  45. package/dist/extensions/plugin/plugin-targeting.d.ts.map +1 -0
  46. package/dist/extensions/plugin-sandbox-bootstrap.js +253 -253
  47. package/dist/extensions/tools/constants.d.ts.map +1 -0
  48. package/dist/{tools → extensions/tools}/definitions.d.ts +1 -1
  49. package/dist/extensions/tools/definitions.d.ts.map +1 -0
  50. package/dist/extensions/tools/executors/apply-patch-parser.d.ts.map +1 -0
  51. package/dist/{tools → extensions/tools}/executors/apply-patch.d.ts +3 -1
  52. package/dist/extensions/tools/executors/apply-patch.d.ts.map +1 -0
  53. package/dist/extensions/tools/executors/bash.d.ts.map +1 -0
  54. package/dist/extensions/tools/executors/editor.d.ts.map +1 -0
  55. package/dist/extensions/tools/executors/file-read.d.ts.map +1 -0
  56. package/dist/extensions/tools/executors/index.d.ts.map +1 -0
  57. package/dist/{tools → extensions/tools}/executors/search.d.ts +1 -1
  58. package/dist/extensions/tools/executors/search.d.ts.map +1 -0
  59. package/dist/extensions/tools/executors/web-fetch.d.ts.map +1 -0
  60. package/dist/extensions/tools/helpers.d.ts.map +1 -0
  61. package/dist/{tools → extensions/tools}/index.d.ts +2 -0
  62. package/dist/extensions/tools/index.d.ts.map +1 -0
  63. package/dist/{tools → extensions/tools}/model-tool-routing.d.ts +1 -1
  64. package/dist/extensions/tools/model-tool-routing.d.ts.map +1 -0
  65. package/dist/{tools → extensions/tools}/presets.d.ts +26 -44
  66. package/dist/extensions/tools/presets.d.ts.map +1 -0
  67. package/dist/extensions/tools/runtime.d.ts +25 -0
  68. package/dist/extensions/tools/runtime.d.ts.map +1 -0
  69. package/dist/extensions/tools/schemas.d.ts.map +1 -0
  70. package/dist/extensions/tools/team/delegated-agent.d.ts.map +1 -0
  71. package/dist/extensions/tools/team/index.d.ts.map +1 -0
  72. package/dist/{team → extensions/tools/team}/multi-agent.d.ts +1 -3
  73. package/dist/extensions/tools/team/multi-agent.d.ts.map +1 -0
  74. package/dist/extensions/tools/team/projections.d.ts.map +1 -0
  75. package/dist/extensions/tools/team/runtime.d.ts.map +1 -0
  76. package/dist/{team → extensions/tools/team}/spawn-agent-tool.d.ts +0 -1
  77. package/dist/extensions/tools/team/spawn-agent-tool.d.ts.map +1 -0
  78. package/dist/extensions/tools/team/subagent-prompts.d.ts.map +1 -0
  79. package/dist/{team → extensions/tools/team}/team-tools.d.ts +1 -0
  80. package/dist/extensions/tools/team/team-tools.d.ts.map +1 -0
  81. package/dist/{tools → extensions/tools}/types.d.ts +4 -3
  82. package/dist/extensions/tools/types.d.ts.map +1 -0
  83. package/dist/{runtime → hooks}/checkpoint-hooks.d.ts +7 -0
  84. package/dist/hooks/checkpoint-hooks.d.ts.map +1 -0
  85. package/dist/{runtime → hooks}/hook-file-hooks.d.ts +4 -3
  86. package/dist/hooks/hook-file-hooks.d.ts.map +1 -0
  87. package/dist/hooks/index.d.ts +0 -1
  88. package/dist/hooks/index.d.ts.map +1 -1
  89. package/dist/hooks/subprocess.d.ts +10 -130
  90. package/dist/hooks/subprocess.d.ts.map +1 -1
  91. package/dist/hub/browser-websocket.d.ts +18 -0
  92. package/dist/hub/browser-websocket.d.ts.map +1 -0
  93. package/dist/hub/client.d.ts +45 -0
  94. package/dist/hub/client.d.ts.map +1 -0
  95. package/dist/hub/connect.d.ts +15 -0
  96. package/dist/hub/connect.d.ts.map +1 -0
  97. package/dist/hub/daemon-entry.d.ts +2 -0
  98. package/dist/hub/daemon-entry.d.ts.map +1 -0
  99. package/dist/hub/daemon-entry.js +1045 -0
  100. package/dist/hub/daemon.d.ts +5 -0
  101. package/dist/hub/daemon.d.ts.map +1 -0
  102. package/dist/hub/defaults.d.ts +13 -0
  103. package/dist/hub/defaults.d.ts.map +1 -0
  104. package/dist/hub/discovery.d.ts +29 -0
  105. package/dist/hub/discovery.d.ts.map +1 -0
  106. package/dist/hub/index.d.ts +15 -0
  107. package/dist/hub/index.d.ts.map +1 -0
  108. package/dist/hub/index.js +1044 -0
  109. package/dist/hub/native-transport.d.ts +17 -0
  110. package/dist/hub/native-transport.d.ts.map +1 -0
  111. package/dist/hub/runtime-handlers.d.ts +11 -0
  112. package/dist/hub/runtime-handlers.d.ts.map +1 -0
  113. package/dist/hub/server.d.ts +86 -0
  114. package/dist/hub/server.d.ts.map +1 -0
  115. package/dist/hub/session-client.d.ts +87 -0
  116. package/dist/hub/session-client.d.ts.map +1 -0
  117. package/dist/hub/start-shared-server.d.ts +19 -0
  118. package/dist/hub/start-shared-server.d.ts.map +1 -0
  119. package/dist/hub/transport.d.ts +8 -0
  120. package/dist/hub/transport.d.ts.map +1 -0
  121. package/dist/hub/ui-client.d.ts +44 -0
  122. package/dist/hub/ui-client.d.ts.map +1 -0
  123. package/dist/hub/workspace.d.ts +4 -0
  124. package/dist/hub/workspace.d.ts.map +1 -0
  125. package/dist/index.d.ts +52 -39
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +557 -533
  128. package/dist/llms/configured-provider-registry.d.ts +28 -0
  129. package/dist/llms/configured-provider-registry.d.ts.map +1 -0
  130. package/dist/llms/provider-defaults.d.ts +27 -0
  131. package/dist/llms/provider-defaults.d.ts.map +1 -0
  132. package/dist/llms/provider-settings.d.ts +202 -0
  133. package/dist/llms/provider-settings.d.ts.map +1 -0
  134. package/dist/llms/runtime-config.d.ts +4 -0
  135. package/dist/llms/runtime-config.d.ts.map +1 -0
  136. package/dist/llms/runtime-registry.d.ts +20 -0
  137. package/dist/llms/runtime-registry.d.ts.map +1 -0
  138. package/dist/llms/runtime-types.d.ts +85 -0
  139. package/dist/llms/runtime-types.d.ts.map +1 -0
  140. package/dist/runtime/history.d.ts +4 -0
  141. package/dist/runtime/history.d.ts.map +1 -0
  142. package/dist/runtime/host.d.ts +8 -0
  143. package/dist/runtime/host.d.ts.map +1 -0
  144. package/dist/runtime/rules.d.ts +1 -0
  145. package/dist/runtime/rules.d.ts.map +1 -1
  146. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  147. package/dist/runtime/runtime-host.d.ts +103 -0
  148. package/dist/runtime/runtime-host.d.ts.map +1 -0
  149. package/dist/{session → runtime}/runtime-oauth-token-manager.d.ts +1 -1
  150. package/dist/{session → runtime}/runtime-oauth-token-manager.d.ts.map +1 -1
  151. package/dist/runtime/session-runtime.d.ts +3 -21
  152. package/dist/runtime/session-runtime.d.ts.map +1 -1
  153. package/dist/{session/session-agent-events.d.ts → services/agent-events.d.ts} +4 -4
  154. package/dist/services/agent-events.d.ts.map +1 -0
  155. package/dist/services/config.d.ts +3 -0
  156. package/dist/services/config.d.ts.map +1 -0
  157. package/dist/services/global-settings.d.ts +12 -0
  158. package/dist/services/global-settings.d.ts.map +1 -0
  159. package/dist/services/local-runtime-bootstrap.d.ts +47 -0
  160. package/dist/services/local-runtime-bootstrap.d.ts.map +1 -0
  161. package/dist/services/plugin-tools.d.ts +16 -0
  162. package/dist/services/plugin-tools.d.ts.map +1 -0
  163. package/dist/{providers → services/providers}/local-provider-registry.d.ts +4 -4
  164. package/dist/services/providers/local-provider-registry.d.ts.map +1 -0
  165. package/dist/{providers → services/providers}/local-provider-service.d.ts +13 -13
  166. package/dist/services/providers/local-provider-service.d.ts.map +1 -0
  167. package/dist/{session → services}/session-artifacts.d.ts +0 -4
  168. package/dist/services/session-artifacts.d.ts.map +1 -0
  169. package/dist/{session/utils/helpers.d.ts → services/session-data.d.ts} +19 -27
  170. package/dist/services/session-data.d.ts.map +1 -0
  171. package/dist/{session → services}/session-telemetry.d.ts +2 -2
  172. package/dist/services/session-telemetry.d.ts.map +1 -0
  173. package/dist/{storage → services/storage}/file-team-store.d.ts +2 -2
  174. package/dist/services/storage/file-team-store.d.ts.map +1 -0
  175. package/dist/{storage → services/storage}/provider-settings-legacy-migration.d.ts +1 -1
  176. package/dist/services/storage/provider-settings-legacy-migration.d.ts.map +1 -0
  177. package/dist/{storage → services/storage}/provider-settings-manager.d.ts +1 -1
  178. package/dist/services/storage/provider-settings-manager.d.ts.map +1 -0
  179. package/dist/{storage → services/storage}/sqlite-session-store.d.ts +3 -3
  180. package/dist/services/storage/sqlite-session-store.d.ts.map +1 -0
  181. package/dist/{storage → services/storage}/sqlite-team-store.d.ts +2 -2
  182. package/dist/services/storage/sqlite-team-store.d.ts.map +1 -0
  183. package/dist/{storage → services/storage}/team-store.d.ts +1 -1
  184. package/dist/services/storage/team-store.d.ts.map +1 -0
  185. package/dist/services/telemetry/ITelemetryAdapter.d.ts.map +1 -0
  186. package/dist/services/telemetry/OpenTelemetryAdapter.d.ts.map +1 -0
  187. package/dist/services/telemetry/OpenTelemetryProvider.d.ts.map +1 -0
  188. package/dist/services/telemetry/TelemetryLoggerSink.d.ts.map +1 -0
  189. package/dist/services/telemetry/TelemetryService.d.ts.map +1 -0
  190. package/dist/services/telemetry/core-events.d.ts.map +1 -0
  191. package/dist/services/telemetry/distinct-id.d.ts.map +1 -0
  192. package/dist/services/telemetry/index.d.ts.map +1 -0
  193. package/dist/services/telemetry/index.js +28 -0
  194. package/dist/{session/utils → services}/usage.d.ts +1 -1
  195. package/dist/services/usage.d.ts.map +1 -0
  196. package/dist/services/workspace/file-indexer.d.ts.map +1 -0
  197. package/dist/services/workspace/index.d.ts.map +1 -0
  198. package/dist/services/workspace/mention-enricher.d.ts.map +1 -0
  199. package/dist/services/workspace-manifest.d.ts +16 -0
  200. package/dist/services/workspace-manifest.d.ts.map +1 -0
  201. package/dist/session/file-session-service.d.ts +4 -1
  202. package/dist/session/file-session-service.d.ts.map +1 -1
  203. package/dist/session/persistence-service.d.ts +15 -25
  204. package/dist/session/persistence-service.d.ts.map +1 -1
  205. package/dist/session/session-manifest-store.d.ts +22 -0
  206. package/dist/session/session-manifest-store.d.ts.map +1 -0
  207. package/dist/session/session-row.d.ts +93 -0
  208. package/dist/session/session-row.d.ts.map +1 -0
  209. package/dist/session/session-service.d.ts +8 -109
  210. package/dist/session/session-service.d.ts.map +1 -1
  211. package/dist/session/session-team-coordination.d.ts +4 -4
  212. package/dist/session/session-team-coordination.d.ts.map +1 -1
  213. package/dist/session/subagent-session-manager.d.ts +36 -0
  214. package/dist/session/subagent-session-manager.d.ts.map +1 -0
  215. package/dist/session/team-persistence-store.d.ts +24 -0
  216. package/dist/session/team-persistence-store.d.ts.map +1 -0
  217. package/dist/transports/hub.d.ts +47 -0
  218. package/dist/transports/hub.d.ts.map +1 -0
  219. package/dist/{session/default-session-manager.d.ts → transports/local.d.ts} +30 -16
  220. package/dist/transports/local.d.ts.map +1 -0
  221. package/dist/transports/remote.d.ts +10 -0
  222. package/dist/transports/remote.d.ts.map +1 -0
  223. package/dist/transports/runtime-host-support.d.ts +22 -0
  224. package/dist/transports/runtime-host-support.d.ts.map +1 -0
  225. package/dist/{chat → types}/chat-schema.d.ts +10 -12
  226. package/dist/types/chat-schema.d.ts.map +1 -0
  227. package/dist/types/config.d.ts +10 -9
  228. package/dist/types/config.d.ts.map +1 -1
  229. package/dist/types/provider-settings.d.ts +4 -5
  230. package/dist/types/provider-settings.d.ts.map +1 -1
  231. package/dist/{session/utils/types.d.ts → types/session.d.ts} +16 -6
  232. package/dist/types/session.d.ts.map +1 -0
  233. package/dist/types/sessions.d.ts +19 -0
  234. package/dist/types/sessions.d.ts.map +1 -1
  235. package/dist/types/storage.d.ts +1 -3
  236. package/dist/types/storage.d.ts.map +1 -1
  237. package/dist/types.d.ts +14 -6
  238. package/dist/types.d.ts.map +1 -1
  239. package/package.json +22 -13
  240. package/src/ClineCore.ts +183 -85
  241. package/src/account/index.ts +3 -3
  242. package/src/account/rpc.ts +12 -12
  243. package/src/auth/cline.ts +1 -1
  244. package/src/auth/codex.ts +1 -1
  245. package/src/auth/oca.ts +1 -1
  246. package/src/cron/index.ts +5 -0
  247. package/src/cron/resource-limiter.ts +46 -0
  248. package/src/cron/schedule-command-service.ts +193 -0
  249. package/src/cron/schedule-service.ts +703 -0
  250. package/src/cron/scheduler.ts +637 -0
  251. package/src/cron/sqlite-schedule-store.ts +708 -0
  252. package/src/extensions/config/agent-config-loader.ts +18 -12
  253. package/src/extensions/config/agent-config-parser.ts +1 -1
  254. package/src/extensions/config/hooks-config-loader.ts +1 -2
  255. package/src/extensions/config/index.ts +0 -4
  256. package/src/extensions/config/runtime-commands.ts +6 -0
  257. package/src/extensions/config/user-instruction-config-loader.ts +1 -4
  258. package/src/extensions/context/agentic-compaction.ts +3 -3
  259. package/src/extensions/context/basic-compaction.ts +2 -2
  260. package/src/extensions/context/compaction-shared.ts +5 -4
  261. package/src/extensions/context/compaction.ts +3 -3
  262. package/src/extensions/plugin/plugin-config-loader.ts +17 -2
  263. package/src/extensions/plugin/plugin-loader.ts +48 -4
  264. package/src/extensions/plugin/plugin-module-import.ts +0 -2
  265. package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +93 -39
  266. package/src/extensions/plugin/plugin-sandbox.ts +47 -27
  267. package/src/extensions/plugin/plugin-targeting.ts +32 -0
  268. package/src/{tools → extensions/tools}/definitions.ts +31 -50
  269. package/src/{tools → extensions/tools}/executors/apply-patch.ts +69 -80
  270. package/src/{tools → extensions/tools}/executors/file-read.ts +29 -4
  271. package/src/{tools → extensions/tools}/executors/search.ts +196 -4
  272. package/src/{tools → extensions/tools}/index.ts +10 -0
  273. package/src/{tools → extensions/tools}/model-tool-routing.ts +1 -1
  274. package/src/{tools → extensions/tools}/presets.ts +31 -47
  275. package/src/extensions/tools/runtime.ts +261 -0
  276. package/src/{tools → extensions/tools}/schemas.ts +4 -2
  277. package/src/{team → extensions/tools/team}/multi-agent.ts +80 -17
  278. package/src/{team → extensions/tools/team}/spawn-agent-tool.ts +1 -7
  279. package/src/{team → extensions/tools/team}/subagent-prompts.ts +2 -2
  280. package/src/{team → extensions/tools/team}/team-tools.ts +84 -53
  281. package/src/{tools → extensions/tools}/types.ts +5 -3
  282. package/src/{runtime → hooks}/checkpoint-hooks.ts +27 -0
  283. package/src/{runtime → hooks}/hook-file-hooks.ts +14 -13
  284. package/src/hooks/index.ts +0 -7
  285. package/src/hooks/subprocess-runner.ts +1 -1
  286. package/src/hooks/subprocess.ts +57 -257
  287. package/src/hub/browser-websocket.ts +137 -0
  288. package/src/hub/client.ts +574 -0
  289. package/src/hub/connect.ts +156 -0
  290. package/src/hub/daemon-entry.ts +87 -0
  291. package/src/hub/daemon.ts +181 -0
  292. package/src/hub/defaults.ts +43 -0
  293. package/src/hub/discovery.ts +247 -0
  294. package/src/hub/index.ts +14 -0
  295. package/src/hub/native-transport.ts +31 -0
  296. package/src/hub/runtime-handlers.ts +140 -0
  297. package/src/hub/server.ts +1888 -0
  298. package/src/hub/session-client.ts +460 -0
  299. package/src/hub/start-shared-server.ts +58 -0
  300. package/src/hub/transport.ts +14 -0
  301. package/src/hub/ui-client.ts +122 -0
  302. package/src/hub/workspace.ts +19 -0
  303. package/src/index.ts +264 -199
  304. package/src/llms/configured-provider-registry.ts +193 -0
  305. package/src/llms/provider-defaults.ts +637 -0
  306. package/src/llms/provider-settings.ts +263 -0
  307. package/src/llms/runtime-config.ts +43 -0
  308. package/src/llms/runtime-registry.ts +171 -0
  309. package/src/llms/runtime-types.ts +121 -0
  310. package/src/runtime/history.ts +237 -0
  311. package/src/runtime/host.ts +200 -0
  312. package/src/runtime/index.ts +1 -0
  313. package/src/runtime/rules.ts +12 -0
  314. package/src/runtime/runtime-builder.ts +37 -29
  315. package/src/runtime/runtime-host.ts +206 -0
  316. package/src/{session → runtime}/runtime-oauth-token-manager.ts +12 -16
  317. package/src/runtime/session-runtime.ts +2 -26
  318. package/src/{session/session-agent-events.ts → services/agent-events.ts} +7 -7
  319. package/src/services/config.ts +5 -0
  320. package/src/services/global-settings.ts +122 -0
  321. package/src/services/local-runtime-bootstrap.ts +318 -0
  322. package/src/services/plugin-tools.ts +85 -0
  323. package/src/{providers → services/providers}/local-provider-registry.ts +6 -6
  324. package/src/{providers → services/providers}/local-provider-service.ts +46 -41
  325. package/src/{session → services}/session-artifacts.ts +7 -19
  326. package/src/{session/utils/helpers.ts → services/session-data.ts} +90 -80
  327. package/src/{session → services}/session-telemetry.ts +7 -9
  328. package/src/services/storage/artifact-store.ts +1 -0
  329. package/src/{storage → services/storage}/file-team-store.ts +2 -2
  330. package/src/{storage → services/storage}/provider-settings-legacy-migration.ts +6 -4
  331. package/src/{storage → services/storage}/provider-settings-manager.ts +3 -3
  332. package/src/services/storage/session-store.ts +1 -0
  333. package/src/{storage → services/storage}/sqlite-session-store.ts +7 -12
  334. package/src/{storage → services/storage}/sqlite-team-store.ts +4 -4
  335. package/src/{storage → services/storage}/team-store.ts +1 -1
  336. package/src/{session/utils → services}/usage.ts +1 -1
  337. package/src/{input → services/workspace}/file-indexer.ts +26 -2
  338. package/src/{input → services/workspace}/mention-enricher.ts +1 -1
  339. package/src/{session → services}/workspace-manifest.ts +18 -0
  340. package/src/session/file-session-service.ts +10 -8
  341. package/src/session/index.ts +10 -23
  342. package/src/session/persistence-service.ts +144 -528
  343. package/src/session/session-manifest-store.ts +158 -0
  344. package/src/session/session-row.ts +199 -0
  345. package/src/session/session-service.ts +43 -389
  346. package/src/session/session-team-coordination.ts +14 -7
  347. package/src/session/subagent-session-manager.ts +397 -0
  348. package/src/session/team-persistence-store.ts +176 -0
  349. package/src/session/workspace-manager.ts +1 -1
  350. package/src/transports/hub.ts +656 -0
  351. package/src/{session/default-session-manager.ts → transports/local.ts} +263 -200
  352. package/src/transports/remote.ts +26 -0
  353. package/src/transports/runtime-host-support.ts +140 -0
  354. package/src/{chat → types}/chat-schema.ts +4 -5
  355. package/src/types/config.ts +10 -9
  356. package/src/types/provider-settings.ts +11 -7
  357. package/src/{session/utils/types.ts → types/session.ts} +16 -5
  358. package/src/types/sessions.ts +21 -0
  359. package/src/types/storage.ts +1 -6
  360. package/src/types.ts +52 -19
  361. package/dist/chat/chat-schema.d.ts.map +0 -1
  362. package/dist/hooks/persistent.d.ts +0 -64
  363. package/dist/hooks/persistent.d.ts.map +0 -1
  364. package/dist/input/file-indexer.d.ts.map +0 -1
  365. package/dist/input/index.d.ts.map +0 -1
  366. package/dist/input/mention-enricher.d.ts.map +0 -1
  367. package/dist/prompt/default-system.d.ts +0 -2
  368. package/dist/prompt/default-system.d.ts.map +0 -1
  369. package/dist/providers/local-provider-registry.d.ts.map +0 -1
  370. package/dist/providers/local-provider-service.d.ts.map +0 -1
  371. package/dist/runtime/checkpoint-hooks.d.ts.map +0 -1
  372. package/dist/runtime/hook-file-hooks.d.ts.map +0 -1
  373. package/dist/session/default-session-manager.d.ts.map +0 -1
  374. package/dist/session/rpc-runtime-ensure.d.ts +0 -53
  375. package/dist/session/rpc-runtime-ensure.d.ts.map +0 -1
  376. package/dist/session/rpc-session-service.d.ts +0 -13
  377. package/dist/session/rpc-session-service.d.ts.map +0 -1
  378. package/dist/session/rpc-spawn-lease.d.ts +0 -8
  379. package/dist/session/rpc-spawn-lease.d.ts.map +0 -1
  380. package/dist/session/session-agent-events.d.ts.map +0 -1
  381. package/dist/session/session-artifacts.d.ts.map +0 -1
  382. package/dist/session/session-config-builder.d.ts +0 -16
  383. package/dist/session/session-config-builder.d.ts.map +0 -1
  384. package/dist/session/session-host.d.ts +0 -15
  385. package/dist/session/session-host.d.ts.map +0 -1
  386. package/dist/session/session-manager.d.ts +0 -62
  387. package/dist/session/session-manager.d.ts.map +0 -1
  388. package/dist/session/session-telemetry.d.ts.map +0 -1
  389. package/dist/session/sqlite-rpc-session-backend.d.ts +0 -31
  390. package/dist/session/sqlite-rpc-session-backend.d.ts.map +0 -1
  391. package/dist/session/utils/helpers.d.ts.map +0 -1
  392. package/dist/session/utils/types.d.ts.map +0 -1
  393. package/dist/session/utils/usage.d.ts.map +0 -1
  394. package/dist/session/workspace-manifest.d.ts +0 -5
  395. package/dist/session/workspace-manifest.d.ts.map +0 -1
  396. package/dist/storage/file-team-store.d.ts.map +0 -1
  397. package/dist/storage/provider-settings-legacy-migration.d.ts.map +0 -1
  398. package/dist/storage/provider-settings-manager.d.ts.map +0 -1
  399. package/dist/storage/sqlite-session-store.d.ts.map +0 -1
  400. package/dist/storage/sqlite-team-store.d.ts.map +0 -1
  401. package/dist/storage/team-store.d.ts.map +0 -1
  402. package/dist/team/delegated-agent.d.ts.map +0 -1
  403. package/dist/team/index.d.ts.map +0 -1
  404. package/dist/team/multi-agent.d.ts.map +0 -1
  405. package/dist/team/projections.d.ts.map +0 -1
  406. package/dist/team/runtime.d.ts.map +0 -1
  407. package/dist/team/spawn-agent-tool.d.ts.map +0 -1
  408. package/dist/team/subagent-prompts.d.ts.map +0 -1
  409. package/dist/team/team-tools.d.ts.map +0 -1
  410. package/dist/telemetry/ITelemetryAdapter.d.ts.map +0 -1
  411. package/dist/telemetry/OpenTelemetryAdapter.d.ts.map +0 -1
  412. package/dist/telemetry/OpenTelemetryProvider.d.ts.map +0 -1
  413. package/dist/telemetry/TelemetryLoggerSink.d.ts.map +0 -1
  414. package/dist/telemetry/TelemetryService.d.ts.map +0 -1
  415. package/dist/telemetry/core-events.d.ts.map +0 -1
  416. package/dist/telemetry/distinct-id.d.ts.map +0 -1
  417. package/dist/telemetry/index.d.ts.map +0 -1
  418. package/dist/telemetry/index.js +0 -15
  419. package/dist/tools/constants.d.ts.map +0 -1
  420. package/dist/tools/definitions.d.ts.map +0 -1
  421. package/dist/tools/executors/apply-patch-parser.d.ts.map +0 -1
  422. package/dist/tools/executors/apply-patch.d.ts.map +0 -1
  423. package/dist/tools/executors/bash.d.ts.map +0 -1
  424. package/dist/tools/executors/editor.d.ts.map +0 -1
  425. package/dist/tools/executors/file-read.d.ts.map +0 -1
  426. package/dist/tools/executors/index.d.ts.map +0 -1
  427. package/dist/tools/executors/search.d.ts.map +0 -1
  428. package/dist/tools/executors/web-fetch.d.ts.map +0 -1
  429. package/dist/tools/helpers.d.ts.map +0 -1
  430. package/dist/tools/index.d.ts.map +0 -1
  431. package/dist/tools/model-tool-routing.d.ts.map +0 -1
  432. package/dist/tools/presets.d.ts.map +0 -1
  433. package/dist/tools/schemas.d.ts.map +0 -1
  434. package/dist/tools/types.d.ts.map +0 -1
  435. package/src/ClineCore.test.ts +0 -150
  436. package/src/account/cline-account-service.test.ts +0 -185
  437. package/src/account/featurebase-token.test.ts +0 -175
  438. package/src/account/rpc.test.ts +0 -63
  439. package/src/auth/bounded-ttl-cache.test.ts +0 -38
  440. package/src/auth/client.test.ts +0 -69
  441. package/src/auth/cline.test.ts +0 -267
  442. package/src/auth/codex.test.ts +0 -170
  443. package/src/auth/oca.test.ts +0 -340
  444. package/src/auth/server.test.ts +0 -287
  445. package/src/auth/utils.test.ts +0 -128
  446. package/src/extensions/config/agent-config-loader.test.ts +0 -236
  447. package/src/extensions/config/hooks-config-loader.test.ts +0 -20
  448. package/src/extensions/config/runtime-commands.test.ts +0 -115
  449. package/src/extensions/config/unified-config-file-watcher.test.ts +0 -196
  450. package/src/extensions/config/user-instruction-config-loader.test.ts +0 -246
  451. package/src/extensions/context/compaction.test.ts +0 -483
  452. package/src/extensions/mcp/config-loader.test.ts +0 -238
  453. package/src/extensions/mcp/manager.test.ts +0 -105
  454. package/src/extensions/plugin/plugin-config-loader.test.ts +0 -182
  455. package/src/extensions/plugin/plugin-loader.test.ts +0 -292
  456. package/src/extensions/plugin/plugin-sandbox.test.ts +0 -423
  457. package/src/hooks/persistent.ts +0 -661
  458. package/src/input/file-indexer.d.ts +0 -11
  459. package/src/input/file-indexer.test.ts +0 -127
  460. package/src/input/mention-enricher.test.ts +0 -85
  461. package/src/prompt/default-system.ts +0 -21
  462. package/src/providers/local-provider-service.test.ts +0 -1062
  463. package/src/runtime/checkpoint-hooks.test.ts +0 -168
  464. package/src/runtime/hook-file-hooks.test.ts +0 -311
  465. package/src/runtime/runtime-builder.team-persistence.test.ts +0 -242
  466. package/src/runtime/runtime-builder.test.ts +0 -616
  467. package/src/runtime/runtime-parity.test.ts +0 -143
  468. package/src/session/default-session-manager.e2e.test.ts +0 -384
  469. package/src/session/default-session-manager.test.ts +0 -2473
  470. package/src/session/persistence-service.test.ts +0 -241
  471. package/src/session/rpc-runtime-ensure.ts +0 -521
  472. package/src/session/rpc-session-service.ts +0 -107
  473. package/src/session/rpc-spawn-lease.test.ts +0 -49
  474. package/src/session/rpc-spawn-lease.ts +0 -122
  475. package/src/session/runtime-oauth-token-manager.test.ts +0 -137
  476. package/src/session/session-config-builder.ts +0 -172
  477. package/src/session/session-host.test.ts +0 -89
  478. package/src/session/session-host.ts +0 -213
  479. package/src/session/session-manager.ts +0 -74
  480. package/src/session/session-service.team-persistence.test.ts +0 -48
  481. package/src/session/sqlite-rpc-session-backend.ts +0 -303
  482. package/src/session/utils/helpers.test.ts +0 -160
  483. package/src/storage/artifact-store.ts +0 -1
  484. package/src/storage/provider-settings-legacy-migration.test.ts +0 -424
  485. package/src/storage/provider-settings-manager.test.ts +0 -191
  486. package/src/storage/session-store.ts +0 -1
  487. package/src/team/multi-agent.lifecycle.test.ts +0 -201
  488. package/src/team/spawn-agent-tool.test.ts +0 -387
  489. package/src/team/team-tools.test.ts +0 -906
  490. package/src/telemetry/OpenTelemetryAdapter.test.ts +0 -157
  491. package/src/telemetry/OpenTelemetryProvider.test.ts +0 -326
  492. package/src/telemetry/TelemetryLoggerSink.test.ts +0 -42
  493. package/src/telemetry/TelemetryService.test.ts +0 -134
  494. package/src/telemetry/distinct-id.test.ts +0 -57
  495. package/src/tools/definitions.test.ts +0 -780
  496. package/src/tools/executors/bash.test.ts +0 -87
  497. package/src/tools/executors/editor.test.ts +0 -35
  498. package/src/tools/executors/file-read.test.ts +0 -49
  499. package/src/tools/model-tool-routing.test.ts +0 -86
  500. package/src/tools/presets.test.ts +0 -70
  501. /package/dist/{tools → extensions/tools}/constants.d.ts +0 -0
  502. /package/dist/{tools → extensions/tools}/executors/apply-patch-parser.d.ts +0 -0
  503. /package/dist/{tools → extensions/tools}/executors/bash.d.ts +0 -0
  504. /package/dist/{tools → extensions/tools}/executors/editor.d.ts +0 -0
  505. /package/dist/{tools → extensions/tools}/executors/file-read.d.ts +0 -0
  506. /package/dist/{tools → extensions/tools}/executors/index.d.ts +0 -0
  507. /package/dist/{tools → extensions/tools}/executors/web-fetch.d.ts +0 -0
  508. /package/dist/{tools → extensions/tools}/helpers.d.ts +0 -0
  509. /package/dist/{tools → extensions/tools}/schemas.d.ts +0 -0
  510. /package/dist/{team → extensions/tools/team}/delegated-agent.d.ts +0 -0
  511. /package/dist/{team → extensions/tools/team}/index.d.ts +0 -0
  512. /package/dist/{team → extensions/tools/team}/projections.d.ts +0 -0
  513. /package/dist/{team → extensions/tools/team}/runtime.d.ts +0 -0
  514. /package/dist/{team → extensions/tools/team}/subagent-prompts.d.ts +0 -0
  515. /package/dist/{telemetry → services/telemetry}/ITelemetryAdapter.d.ts +0 -0
  516. /package/dist/{telemetry → services/telemetry}/OpenTelemetryAdapter.d.ts +0 -0
  517. /package/dist/{telemetry → services/telemetry}/OpenTelemetryProvider.d.ts +0 -0
  518. /package/dist/{telemetry → services/telemetry}/TelemetryLoggerSink.d.ts +0 -0
  519. /package/dist/{telemetry → services/telemetry}/TelemetryService.d.ts +0 -0
  520. /package/dist/{telemetry → services/telemetry}/core-events.d.ts +0 -0
  521. /package/dist/{telemetry → services/telemetry}/distinct-id.d.ts +0 -0
  522. /package/dist/{telemetry → services/telemetry}/index.d.ts +0 -0
  523. /package/dist/{input → services/workspace}/file-indexer.d.ts +0 -0
  524. /package/dist/{input → services/workspace}/index.d.ts +0 -0
  525. /package/dist/{input → services/workspace}/mention-enricher.d.ts +0 -0
  526. /package/src/{tools → extensions/tools}/constants.ts +0 -0
  527. /package/src/{tools → extensions/tools}/executors/apply-patch-parser.ts +0 -0
  528. /package/src/{tools → extensions/tools}/executors/bash.ts +0 -0
  529. /package/src/{tools → extensions/tools}/executors/editor.ts +0 -0
  530. /package/src/{tools → extensions/tools}/executors/index.ts +0 -0
  531. /package/src/{tools → extensions/tools}/executors/web-fetch.ts +0 -0
  532. /package/src/{tools → extensions/tools}/helpers.ts +0 -0
  533. /package/src/{team → extensions/tools/team}/delegated-agent.ts +0 -0
  534. /package/src/{team → extensions/tools/team}/index.ts +0 -0
  535. /package/src/{team → extensions/tools/team}/projections.ts +0 -0
  536. /package/src/{team → extensions/tools/team}/runtime.ts +0 -0
  537. /package/src/{storage → services/storage}/index.ts +0 -0
  538. /package/src/{telemetry → services/telemetry}/ITelemetryAdapter.ts +0 -0
  539. /package/src/{telemetry → services/telemetry}/OpenTelemetryAdapter.ts +0 -0
  540. /package/src/{telemetry → services/telemetry}/OpenTelemetryProvider.ts +0 -0
  541. /package/src/{telemetry → services/telemetry}/TelemetryLoggerSink.ts +0 -0
  542. /package/src/{telemetry → services/telemetry}/TelemetryService.ts +0 -0
  543. /package/src/{telemetry → services/telemetry}/core-events.ts +0 -0
  544. /package/src/{telemetry → services/telemetry}/distinct-id.ts +0 -0
  545. /package/src/{telemetry → services/telemetry}/index.ts +0 -0
  546. /package/src/{input → services/workspace}/index.ts +0 -0
@@ -0,0 +1,1888 @@
1
+ import http from "node:http";
2
+ import { URL } from "node:url";
3
+ import type {
4
+ HubClientRecord,
5
+ HubClientRegistration,
6
+ HubCommandEnvelope,
7
+ HubEventEnvelope,
8
+ HubReplyEnvelope,
9
+ SessionRecord as HubSessionRecord,
10
+ HubToolExecutorName,
11
+ JsonValue,
12
+ SessionParticipant,
13
+ TeamProgressProjectionEvent,
14
+ ToolApprovalRequest,
15
+ ToolContext,
16
+ } from "@clinebot/shared";
17
+ import { createSessionId } from "@clinebot/shared";
18
+ import { WebSocketServer } from "ws";
19
+ import { HubScheduleCommandService } from "../cron/schedule-command-service";
20
+ import {
21
+ type HubScheduleRuntimeHandlers,
22
+ HubScheduleService,
23
+ type HubScheduleServiceOptions,
24
+ } from "../cron/schedule-service";
25
+ import type { ToolExecutors } from "../extensions/tools";
26
+ import { parseHookEventPayload } from "../hooks";
27
+ import type {
28
+ RuntimeHost,
29
+ RuntimeSessionConfig,
30
+ } from "../runtime/runtime-host";
31
+ import { SqliteSessionStore } from "../services/storage/sqlite-session-store";
32
+ import { CoreSessionService } from "../session/session-service";
33
+ import { LocalRuntimeHost } from "../transports/local";
34
+ import { readPersistedMessagesFile } from "../transports/runtime-host-support";
35
+ import type { CoreSessionEvent } from "../types/events";
36
+ import type { SessionRecord as LocalSessionRecord } from "../types/sessions";
37
+ import { BrowserWebSocketHubAdapter } from "./browser-websocket";
38
+ import { verifyHubConnection } from "./client";
39
+ import {
40
+ clearHubDiscovery,
41
+ createHubServerUrl,
42
+ type HubOwnerContext,
43
+ type HubServerDiscoveryRecord,
44
+ probeHubServer,
45
+ readHubDiscovery,
46
+ resolveHubBuildId,
47
+ resolveHubOwnerContext,
48
+ withHubStartupLock,
49
+ writeHubDiscovery,
50
+ } from "./discovery";
51
+ import {
52
+ type NativeHubTransport,
53
+ NativeHubTransportAdapter,
54
+ } from "./native-transport";
55
+
56
+ type NodeWebSocketLike = {
57
+ send(data: string): void;
58
+ on(event: "message", listener: (data: unknown) => void): void;
59
+ on(event: "close", listener: () => void): void;
60
+ once(event: "close", listener: () => void): void;
61
+ };
62
+
63
+ type HubSessionState = {
64
+ createdByClientId: string;
65
+ participants: Map<string, SessionParticipant>;
66
+ };
67
+
68
+ function decodeSocketData(data: unknown): string {
69
+ if (typeof data === "string") {
70
+ return data;
71
+ }
72
+ if (data instanceof Uint8Array) {
73
+ return Buffer.from(data).toString();
74
+ }
75
+ if (data instanceof ArrayBuffer) {
76
+ return Buffer.from(data).toString();
77
+ }
78
+ if (Array.isArray(data)) {
79
+ return Buffer.concat(data.map((chunk) => Buffer.from(chunk))).toString();
80
+ }
81
+ return String(data);
82
+ }
83
+
84
+ function wrapWsSocket(socket: NodeWebSocketLike) {
85
+ return {
86
+ send(data: string): void {
87
+ socket.send(data);
88
+ },
89
+ addEventListener(
90
+ type: "message" | "close",
91
+ listener: (...args: never[]) => void,
92
+ ): void {
93
+ if (type === "message") {
94
+ socket.on("message", (data: unknown) => {
95
+ (listener as (event: { data: string }) => void)({
96
+ data: decodeSocketData(data),
97
+ });
98
+ });
99
+ return;
100
+ }
101
+ socket.on("close", listener as () => void);
102
+ },
103
+ removeEventListener(): void {},
104
+ };
105
+ }
106
+
107
+ function formatHubUptime(ms: number): string {
108
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
109
+ const days = Math.floor(totalSeconds / 86_400);
110
+ const hours = Math.floor((totalSeconds % 86_400) / 3_600);
111
+ const minutes = Math.floor((totalSeconds % 3_600) / 60);
112
+ const seconds = totalSeconds % 60;
113
+ if (days > 0) {
114
+ return `${days}d ${hours}h`;
115
+ }
116
+ if (hours > 0) {
117
+ return `${hours}h ${minutes}m`;
118
+ }
119
+ if (minutes > 0) {
120
+ return `${minutes}m ${seconds}s`;
121
+ }
122
+ return `${seconds}s`;
123
+ }
124
+
125
+ function mapLocalStatusToHubStatus(
126
+ status: LocalSessionRecord["status"],
127
+ ): HubSessionRecord["status"] {
128
+ switch (status) {
129
+ case "completed":
130
+ return "completed";
131
+ case "failed":
132
+ return "failed";
133
+ case "cancelled":
134
+ return "aborted";
135
+ default:
136
+ return "running";
137
+ }
138
+ }
139
+
140
+ function cloneSessionMetadata(
141
+ session: LocalSessionRecord,
142
+ ): Record<string, JsonValue | undefined> | undefined {
143
+ const metadata =
144
+ session.metadata && typeof session.metadata === "object"
145
+ ? (JSON.parse(JSON.stringify(session.metadata)) as Record<
146
+ string,
147
+ JsonValue | undefined
148
+ >)
149
+ : ({} as Record<string, JsonValue | undefined>);
150
+ if (session.parentSessionId?.trim())
151
+ metadata.parentSessionId = session.parentSessionId;
152
+ if (session.parentAgentId?.trim())
153
+ metadata.parentAgentId = session.parentAgentId;
154
+ if (session.agentId?.trim()) metadata.agentId = session.agentId;
155
+ if (session.conversationId?.trim())
156
+ metadata.conversationId = session.conversationId;
157
+ if (session.messagesPath?.trim())
158
+ metadata.messagesPath = session.messagesPath;
159
+ if (session.prompt?.trim()) metadata.prompt = session.prompt;
160
+ return Object.keys(metadata).length > 0 ? metadata : undefined;
161
+ }
162
+
163
+ function toHubSessionRecord(
164
+ session: LocalSessionRecord,
165
+ state?: HubSessionState,
166
+ ): HubSessionRecord {
167
+ return {
168
+ sessionId: session.sessionId,
169
+ workspaceRoot: session.workspaceRoot,
170
+ cwd: session.cwd,
171
+ createdAt: Date.parse(session.startedAt),
172
+ updatedAt: Date.parse(session.updatedAt),
173
+ createdByClientId: state?.createdByClientId ?? "hub",
174
+ status: mapLocalStatusToHubStatus(session.status),
175
+ participants: state ? [...state.participants.values()] : [],
176
+ metadata: cloneSessionMetadata(session),
177
+ runtimeOptions: {
178
+ enableTools: session.enableTools,
179
+ enableSpawn: session.enableSpawn,
180
+ enableTeams: session.enableTeams,
181
+ mode:
182
+ typeof session.metadata?.mode === "string"
183
+ ? (session.metadata.mode as "act" | "plan" | "yolo")
184
+ : undefined,
185
+ systemPrompt:
186
+ typeof session.metadata?.systemPrompt === "string"
187
+ ? session.metadata.systemPrompt
188
+ : undefined,
189
+ },
190
+ runtimeSession: session.agentId
191
+ ? {
192
+ agentId: session.agentId,
193
+ team: session.teamName ? { teamId: session.teamName } : undefined,
194
+ }
195
+ : undefined,
196
+ };
197
+ }
198
+
199
+ function eventNameForScheduleCommand(
200
+ command: HubCommandEnvelope["command"],
201
+ ): HubEventEnvelope["event"] | undefined {
202
+ switch (command) {
203
+ case "schedule.create":
204
+ return "schedule.created";
205
+ case "schedule.update":
206
+ case "schedule.enable":
207
+ case "schedule.disable":
208
+ return "schedule.updated";
209
+ case "schedule.delete":
210
+ return "schedule.deleted";
211
+ case "schedule.trigger":
212
+ return "schedule.triggered";
213
+ default:
214
+ return undefined;
215
+ }
216
+ }
217
+
218
+ function extractAssistantText(content: unknown): string | undefined {
219
+ if (typeof content === "string") {
220
+ const trimmed = content.trim();
221
+ return trimmed || undefined;
222
+ }
223
+ if (!Array.isArray(content)) {
224
+ return undefined;
225
+ }
226
+ const text = content
227
+ .map((part) => {
228
+ if (
229
+ part &&
230
+ typeof part === "object" &&
231
+ "type" in part &&
232
+ (part as { type?: unknown }).type === "text" &&
233
+ "text" in part &&
234
+ typeof (part as { text?: unknown }).text === "string"
235
+ ) {
236
+ return (part as { text: string }).text.trim();
237
+ }
238
+ return "";
239
+ })
240
+ .filter(Boolean)
241
+ .join("\n")
242
+ .trim();
243
+ return text || undefined;
244
+ }
245
+
246
+ const MAX_NOTIFICATION_BODY_BYTES = 120;
247
+ const NOTIFICATION_BODY_ELLIPSIS = "...";
248
+
249
+ export function truncateNotificationBody(value: string): string {
250
+ const trimmed = value.trim();
251
+ if (!trimmed) {
252
+ return "";
253
+ }
254
+ if (Buffer.byteLength(trimmed, "utf8") <= MAX_NOTIFICATION_BODY_BYTES) {
255
+ return trimmed;
256
+ }
257
+ const budget =
258
+ MAX_NOTIFICATION_BODY_BYTES -
259
+ Buffer.byteLength(NOTIFICATION_BODY_ELLIPSIS, "utf8");
260
+ if (budget <= 0) {
261
+ return NOTIFICATION_BODY_ELLIPSIS;
262
+ }
263
+ let truncated = "";
264
+ for (const char of trimmed) {
265
+ if (Buffer.byteLength(truncated + char, "utf8") > budget) {
266
+ break;
267
+ }
268
+ truncated += char;
269
+ }
270
+ return `${truncated}${NOTIFICATION_BODY_ELLIPSIS}`;
271
+ }
272
+
273
+ async function buildCompletionNotification(
274
+ session: HubSessionRecord | undefined,
275
+ ): Promise<{
276
+ title: string;
277
+ body: string;
278
+ severity: "info";
279
+ }> {
280
+ const sessionId = session?.sessionId?.trim() || "unknown";
281
+ const messagesPath =
282
+ typeof session?.metadata?.messagesPath === "string"
283
+ ? session.metadata.messagesPath
284
+ : undefined;
285
+ const messages = await readPersistedMessagesFile(messagesPath);
286
+ const latestAssistantText = [...messages]
287
+ .reverse()
288
+ .find((message) => message.role === "assistant");
289
+ const assistantReply = latestAssistantText
290
+ ? extractAssistantText(latestAssistantText.content)
291
+ : undefined;
292
+ const workspaceRoot = session?.workspaceRoot?.trim() || "workspace";
293
+ const fallback =
294
+ typeof session?.metadata?.prompt === "string"
295
+ ? session.metadata.prompt.trim()
296
+ : workspaceRoot;
297
+ return {
298
+ title: `Task completed (${sessionId})`,
299
+ body: truncateNotificationBody(
300
+ assistantReply && assistantReply.length > 0
301
+ ? assistantReply
302
+ : fallback.length > 0
303
+ ? fallback
304
+ : workspaceRoot,
305
+ ),
306
+ severity: "info",
307
+ };
308
+ }
309
+
310
+ function isHubToolExecutorName(value: unknown): value is HubToolExecutorName {
311
+ return (
312
+ value === "readFile" ||
313
+ value === "search" ||
314
+ value === "bash" ||
315
+ value === "webFetch" ||
316
+ value === "editor" ||
317
+ value === "applyPatch" ||
318
+ value === "skills" ||
319
+ value === "askQuestion" ||
320
+ value === "submit"
321
+ );
322
+ }
323
+
324
+ function formatHubStartupError(
325
+ error: unknown,
326
+ context: {
327
+ host: string;
328
+ port: number;
329
+ pathname: string;
330
+ },
331
+ ): Error {
332
+ const code =
333
+ error &&
334
+ typeof error === "object" &&
335
+ "code" in error &&
336
+ typeof (error as { code?: unknown }).code === "string"
337
+ ? (error as { code: string }).code
338
+ : undefined;
339
+ const message =
340
+ error instanceof Error
341
+ ? error.message
342
+ : typeof error === "string"
343
+ ? error
344
+ : "Unknown startup error";
345
+ const details = `Failed to start hub server on ${context.host}:${context.port}${context.pathname}: ${message}`;
346
+ const wrapped = new Error(code ? `${details} (${code})` : details);
347
+ if (code) {
348
+ (error as Error & { code?: string }).code = code;
349
+ (wrapped as Error & { code?: string }).code = code;
350
+ }
351
+ if (error instanceof Error && error.stack) {
352
+ wrapped.stack = `${wrapped.name}: ${wrapped.message}\nCaused by: ${error.stack}`;
353
+ }
354
+ return wrapped;
355
+ }
356
+
357
+ function serializeToolContext(context: ToolContext): Record<string, unknown> {
358
+ return {
359
+ agentId: context.agentId,
360
+ conversationId: context.conversationId,
361
+ iteration: context.iteration,
362
+ metadata: context.metadata,
363
+ };
364
+ }
365
+
366
+ function createCapabilityBackedToolExecutors(
367
+ targetClientId: string,
368
+ executors: HubToolExecutorName[],
369
+ requestCapability: (
370
+ sessionId: string,
371
+ capabilityName: string,
372
+ payload: Record<string, unknown>,
373
+ targetClientId: string,
374
+ ) => Promise<Record<string, unknown> | undefined>,
375
+ ): Partial<ToolExecutors> {
376
+ const available = new Set(executors);
377
+ const invoke = async (
378
+ executor: HubToolExecutorName,
379
+ args: unknown[],
380
+ context: ToolContext,
381
+ ): Promise<unknown> => {
382
+ const response = await requestCapability(
383
+ context.conversationId,
384
+ `tool_executor.${executor}`,
385
+ {
386
+ executor,
387
+ args,
388
+ context: serializeToolContext(context),
389
+ },
390
+ targetClientId,
391
+ );
392
+ return response?.result;
393
+ };
394
+
395
+ return {
396
+ ...(available.has("readFile")
397
+ ? {
398
+ readFile: async (request, context) =>
399
+ (await invoke("readFile", [request], context)) as Awaited<
400
+ ReturnType<NonNullable<ToolExecutors["readFile"]>>
401
+ >,
402
+ }
403
+ : {}),
404
+ ...(available.has("search")
405
+ ? {
406
+ search: async (query, cwd, context) =>
407
+ String((await invoke("search", [query, cwd], context)) ?? ""),
408
+ }
409
+ : {}),
410
+ ...(available.has("bash")
411
+ ? {
412
+ bash: async (command, cwd, context) =>
413
+ String((await invoke("bash", [command, cwd], context)) ?? ""),
414
+ }
415
+ : {}),
416
+ ...(available.has("webFetch")
417
+ ? {
418
+ webFetch: async (url, prompt, context) =>
419
+ String((await invoke("webFetch", [url, prompt], context)) ?? ""),
420
+ }
421
+ : {}),
422
+ ...(available.has("editor")
423
+ ? {
424
+ editor: async (input, cwd, context) =>
425
+ String((await invoke("editor", [input, cwd], context)) ?? ""),
426
+ }
427
+ : {}),
428
+ ...(available.has("applyPatch")
429
+ ? {
430
+ applyPatch: async (input, cwd, context) =>
431
+ String((await invoke("applyPatch", [input, cwd], context)) ?? ""),
432
+ }
433
+ : {}),
434
+ ...(available.has("skills")
435
+ ? {
436
+ skills: async (skill, args, context) =>
437
+ String((await invoke("skills", [skill, args], context)) ?? ""),
438
+ }
439
+ : {}),
440
+ ...(available.has("askQuestion")
441
+ ? {
442
+ askQuestion: async (question, options, context) =>
443
+ String(
444
+ (await invoke("askQuestion", [question, options], context)) ?? "",
445
+ ),
446
+ }
447
+ : {}),
448
+ ...(available.has("submit")
449
+ ? {
450
+ submit: async (summary, verified, context) =>
451
+ String(
452
+ (await invoke("submit", [summary, verified], context)) ?? "",
453
+ ),
454
+ }
455
+ : {}),
456
+ };
457
+ }
458
+
459
+ /** @internal Exported for unit testing fetch/runtime wiring. */
460
+ export class HubServerTransport implements NativeHubTransport {
461
+ private readonly clients = new Map<string, HubClientRecord>();
462
+ private readonly listeners = new Map<
463
+ string,
464
+ Set<{ sessionId?: string; listener: (event: HubEventEnvelope) => void }>
465
+ >();
466
+ private readonly sessionState = new Map<string, HubSessionState>();
467
+ private readonly pendingApprovals = new Map<
468
+ string,
469
+ {
470
+ sessionId: string;
471
+ resolve: (result: { approved: boolean; reason?: string }) => void;
472
+ }
473
+ >();
474
+ private readonly pendingCapabilityRequests = new Map<
475
+ string,
476
+ {
477
+ sessionId: string;
478
+ capabilityName: string;
479
+ resolve: (result: {
480
+ ok: boolean;
481
+ payload?: Record<string, unknown>;
482
+ error?: string;
483
+ }) => void;
484
+ }
485
+ >();
486
+ private readonly schedules: HubScheduleService;
487
+ private readonly scheduleCommands: HubScheduleCommandService;
488
+ private readonly sessionHost: RuntimeHost;
489
+ private readonly hubId = createSessionId("hub_");
490
+ private readonly startedAtMs = Date.now();
491
+
492
+ constructor(readonly options: HubWebSocketServerOptions) {
493
+ this.sessionHost =
494
+ options.sessionHost ??
495
+ new LocalRuntimeHost({
496
+ sessionService: new CoreSessionService(new SqliteSessionStore()),
497
+ fetch: options.fetch,
498
+ });
499
+ this.schedules = new HubScheduleService({
500
+ ...options.scheduleOptions,
501
+ runtimeHandlers: options.runtimeHandlers,
502
+ eventPublisher: (eventType, payload) => {
503
+ const mapped =
504
+ eventType === "schedule.execution.completed"
505
+ ? "schedule.execution_completed"
506
+ : eventType === "schedule.execution.failed"
507
+ ? "schedule.execution_failed"
508
+ : undefined;
509
+ if (!mapped) {
510
+ return;
511
+ }
512
+ this.publish({
513
+ version: "v1",
514
+ event: mapped,
515
+ eventId: createSessionId("hevt_"),
516
+ timestamp: Date.now(),
517
+ payload:
518
+ payload && typeof payload === "object"
519
+ ? (payload as Record<string, unknown>)
520
+ : undefined,
521
+ });
522
+ },
523
+ });
524
+ this.scheduleCommands = new HubScheduleCommandService(this.schedules);
525
+ this.sessionHost.subscribe((event) => {
526
+ void this.handleSessionEvent(event);
527
+ });
528
+ }
529
+
530
+ getHubId(): string {
531
+ return this.hubId;
532
+ }
533
+
534
+ async start(): Promise<void> {
535
+ await this.schedules.start();
536
+ }
537
+
538
+ async stop(): Promise<void> {
539
+ for (const approval of this.pendingApprovals.values()) {
540
+ approval.resolve({
541
+ approved: false,
542
+ reason: "Hub shutting down before approval was resolved.",
543
+ });
544
+ }
545
+ this.pendingApprovals.clear();
546
+ for (const pending of this.pendingCapabilityRequests.values()) {
547
+ pending.resolve({
548
+ ok: false,
549
+ error: "Hub shutting down before capability request was resolved.",
550
+ });
551
+ }
552
+ this.pendingCapabilityRequests.clear();
553
+ await this.sessionHost.dispose("hub_server_stop");
554
+ await this.schedules.dispose();
555
+ }
556
+
557
+ async handleCommand(envelope: HubCommandEnvelope): Promise<HubReplyEnvelope> {
558
+ const uptimeMs = Date.now() - this.startedAtMs;
559
+ console.error(
560
+ `[hub] command=${envelope.command} uptime=${formatHubUptime(uptimeMs)} client=${envelope.clientId ?? "unknown"} session=${envelope.sessionId ?? "-"}`,
561
+ );
562
+ switch (envelope.command) {
563
+ case "client.register":
564
+ return this.handleClientRegister(envelope);
565
+ case "client.update":
566
+ return this.handleClientUpdate(envelope);
567
+ case "client.unregister":
568
+ return this.handleClientUnregister(envelope);
569
+ case "client.list":
570
+ return {
571
+ version: envelope.version,
572
+ requestId: envelope.requestId,
573
+ ok: true,
574
+ payload: { clients: [...this.clients.values()] },
575
+ };
576
+ case "session.create":
577
+ return await this.handleSessionCreate(envelope);
578
+ case "session.attach":
579
+ return await this.handleSessionAttach(envelope);
580
+ case "session.detach":
581
+ return await this.handleSessionDetach(envelope);
582
+ case "session.get":
583
+ return await this.handleSessionGet(envelope);
584
+ case "session.list":
585
+ return await this.handleSessionList(envelope);
586
+ case "session.update":
587
+ return await this.handleSessionUpdate(envelope);
588
+ case "session.delete":
589
+ return await this.handleSessionDelete(envelope);
590
+ case "session.hook":
591
+ return await this.handleSessionHook(envelope);
592
+ case "run.start":
593
+ case "session.send_input":
594
+ return await this.handleSessionInput(envelope);
595
+ case "run.abort":
596
+ return await this.handleRunAbort(envelope);
597
+ case "capability.request":
598
+ return await this.handleCapabilityRequest(envelope);
599
+ case "approval.respond":
600
+ return await this.handleApprovalRespond(envelope);
601
+ case "capability.respond":
602
+ return await this.handleCapabilityRespond(envelope);
603
+ case "ui.notify":
604
+ this.publish(this.buildEvent("ui.notify", envelope.payload ?? {}));
605
+ return {
606
+ version: envelope.version,
607
+ requestId: envelope.requestId,
608
+ ok: true,
609
+ };
610
+ case "ui.show_window":
611
+ this.publish(this.buildEvent("ui.show_window", envelope.payload ?? {}));
612
+ return {
613
+ version: envelope.version,
614
+ requestId: envelope.requestId,
615
+ ok: true,
616
+ };
617
+ default: {
618
+ const reply = await this.scheduleCommands.handleCommand(envelope);
619
+ if (reply.ok) {
620
+ const event = eventNameForScheduleCommand(envelope.command);
621
+ if (event) {
622
+ this.publish({
623
+ version: "v1",
624
+ event,
625
+ eventId: createSessionId("hevt_"),
626
+ timestamp: Date.now(),
627
+ payload: reply.payload,
628
+ });
629
+ }
630
+ }
631
+ return reply;
632
+ }
633
+ }
634
+ }
635
+
636
+ private handleClientRegister(envelope: HubCommandEnvelope): HubReplyEnvelope {
637
+ const payload = envelope.payload as HubClientRegistration | undefined;
638
+ const clientId =
639
+ payload?.clientId?.trim() ||
640
+ envelope.clientId?.trim() ||
641
+ createSessionId("client_");
642
+ this.clients.set(clientId, {
643
+ clientId,
644
+ clientType: payload?.clientType ?? "unknown",
645
+ displayName: payload?.displayName,
646
+ actorKind: payload?.actorKind ?? "client",
647
+ connectedAt: Date.now(),
648
+ lastSeenAt: Date.now(),
649
+ transport: payload?.transport ?? "native",
650
+ capabilities: payload?.capabilities ?? [],
651
+ metadata: payload?.metadata,
652
+ workspaceContext: payload?.workspaceContext,
653
+ });
654
+ this.publish(
655
+ this.buildEvent("hub.client.registered", {
656
+ clientId,
657
+ clientType: payload?.clientType ?? "unknown",
658
+ displayName: payload?.displayName,
659
+ connectedAt: Date.now(),
660
+ }),
661
+ );
662
+ return {
663
+ version: envelope.version,
664
+ requestId: envelope.requestId,
665
+ ok: true,
666
+ payload: { clientId },
667
+ };
668
+ }
669
+
670
+ private handleClientUpdate(envelope: HubCommandEnvelope): HubReplyEnvelope {
671
+ const clientId = envelope.clientId?.trim();
672
+ const client = clientId ? this.clients.get(clientId) : undefined;
673
+ if (!clientId || !client) {
674
+ return {
675
+ version: envelope.version,
676
+ requestId: envelope.requestId,
677
+ ok: false,
678
+ error: {
679
+ code: "client_not_found",
680
+ message: "Client is not registered with this hub.",
681
+ },
682
+ };
683
+ }
684
+ const metadata =
685
+ envelope.payload?.metadata &&
686
+ typeof envelope.payload.metadata === "object" &&
687
+ !Array.isArray(envelope.payload.metadata)
688
+ ? (envelope.payload.metadata as Record<string, JsonValue | undefined>)
689
+ : undefined;
690
+ client.lastSeenAt = Date.now();
691
+ if (metadata) {
692
+ client.metadata = JSON.parse(JSON.stringify(metadata)) as Record<
693
+ string,
694
+ JsonValue | undefined
695
+ >;
696
+ }
697
+ return {
698
+ version: envelope.version,
699
+ requestId: envelope.requestId,
700
+ ok: true,
701
+ };
702
+ }
703
+
704
+ private handleClientUnregister(
705
+ envelope: HubCommandEnvelope,
706
+ ): HubReplyEnvelope {
707
+ const clientId = envelope.clientId?.trim();
708
+ if (clientId) {
709
+ this.clients.delete(clientId);
710
+ this.listeners.delete(clientId);
711
+ }
712
+ if (clientId) {
713
+ this.publish(this.buildEvent("hub.client.disconnected", { clientId }));
714
+ }
715
+ return {
716
+ version: envelope.version,
717
+ requestId: envelope.requestId,
718
+ ok: true,
719
+ };
720
+ }
721
+
722
+ private buildEvent(
723
+ event: HubEventEnvelope["event"],
724
+ payload?: Record<string, unknown>,
725
+ sessionId?: string,
726
+ ): HubEventEnvelope {
727
+ return {
728
+ version: "v1",
729
+ event,
730
+ eventId: createSessionId("hevt_"),
731
+ sessionId,
732
+ timestamp: Date.now(),
733
+ payload,
734
+ };
735
+ }
736
+
737
+ private async readHubSessionRecord(
738
+ sessionId: string,
739
+ ): Promise<HubSessionRecord | undefined> {
740
+ const session = await this.sessionHost.get(sessionId);
741
+ if (!session) {
742
+ return undefined;
743
+ }
744
+ return toHubSessionRecord(session, this.sessionState.get(sessionId));
745
+ }
746
+
747
+ private ensureSessionState(
748
+ sessionId: string,
749
+ clientId: string,
750
+ role: SessionParticipant["role"],
751
+ ): HubSessionState {
752
+ const existing = this.sessionState.get(sessionId);
753
+ if (existing) {
754
+ if (!existing.participants.has(clientId)) {
755
+ existing.participants.set(clientId, {
756
+ clientId,
757
+ attachedAt: Date.now(),
758
+ role,
759
+ });
760
+ }
761
+ return existing;
762
+ }
763
+ const state: HubSessionState = {
764
+ createdByClientId: clientId,
765
+ participants: new Map([
766
+ [
767
+ clientId,
768
+ {
769
+ clientId,
770
+ attachedAt: Date.now(),
771
+ role,
772
+ },
773
+ ],
774
+ ]),
775
+ };
776
+ this.sessionState.set(sessionId, state);
777
+ return state;
778
+ }
779
+
780
+ private async requestCapability(
781
+ sessionId: string,
782
+ capabilityName: string,
783
+ payload: Record<string, unknown>,
784
+ targetClientId: string,
785
+ ): Promise<Record<string, unknown> | undefined> {
786
+ const requestId = createSessionId("capreq_");
787
+ return await new Promise((resolve, reject) => {
788
+ this.pendingCapabilityRequests.set(requestId, {
789
+ sessionId,
790
+ capabilityName,
791
+ resolve: (result) => {
792
+ if (!result.ok) {
793
+ reject(
794
+ new Error(
795
+ result.error ||
796
+ `Capability ${capabilityName} was rejected by ${targetClientId}.`,
797
+ ),
798
+ );
799
+ return;
800
+ }
801
+ resolve(result.payload);
802
+ },
803
+ });
804
+ this.publish(
805
+ this.buildEvent(
806
+ "capability.requested",
807
+ {
808
+ requestId,
809
+ targetClientId,
810
+ capabilityName,
811
+ payload,
812
+ },
813
+ sessionId,
814
+ ),
815
+ );
816
+ });
817
+ }
818
+
819
+ private async handleSessionCreate(
820
+ envelope: HubCommandEnvelope,
821
+ ): Promise<HubReplyEnvelope> {
822
+ const payload =
823
+ envelope.payload && typeof envelope.payload === "object"
824
+ ? envelope.payload
825
+ : {};
826
+ const metadata =
827
+ payload.metadata && typeof payload.metadata === "object"
828
+ ? JSON.parse(JSON.stringify(payload.metadata))
829
+ : {};
830
+ const sessionConfig =
831
+ payload.sessionConfig && typeof payload.sessionConfig === "object"
832
+ ? (JSON.parse(
833
+ JSON.stringify(payload.sessionConfig),
834
+ ) as Partial<RuntimeSessionConfig>)
835
+ : undefined;
836
+ const runtimeOptions =
837
+ payload.runtimeOptions && typeof payload.runtimeOptions === "object"
838
+ ? (payload.runtimeOptions as Record<string, unknown>)
839
+ : {};
840
+ if (typeof sessionConfig?.mode === "string") {
841
+ metadata.mode = sessionConfig.mode;
842
+ } else if (typeof runtimeOptions.mode === "string") {
843
+ metadata.mode = runtimeOptions.mode;
844
+ }
845
+ if (typeof sessionConfig?.systemPrompt === "string") {
846
+ metadata.systemPrompt = sessionConfig.systemPrompt;
847
+ } else if (typeof runtimeOptions.systemPrompt === "string") {
848
+ metadata.systemPrompt = runtimeOptions.systemPrompt;
849
+ }
850
+ if (sessionConfig?.checkpoint?.enabled === true) {
851
+ metadata.checkpointEnabled = true;
852
+ } else if (runtimeOptions.checkpointEnabled === true) {
853
+ metadata.checkpointEnabled = true;
854
+ }
855
+ const modelSelection =
856
+ payload.modelSelection && typeof payload.modelSelection === "object"
857
+ ? (payload.modelSelection as Record<string, unknown>)
858
+ : {};
859
+ const workspaceRoot =
860
+ typeof payload.workspaceRoot === "string" && payload.workspaceRoot.trim()
861
+ ? payload.workspaceRoot.trim()
862
+ : typeof payload.cwd === "string" && payload.cwd.trim()
863
+ ? payload.cwd.trim()
864
+ : "";
865
+ if (!workspaceRoot) {
866
+ return {
867
+ version: envelope.version,
868
+ requestId: envelope.requestId,
869
+ ok: false,
870
+ error: {
871
+ code: "invalid_session_create",
872
+ message: "session.create requires workspaceRoot or cwd",
873
+ },
874
+ };
875
+ }
876
+ const clientId = envelope.clientId?.trim() || "hub-client";
877
+ const advertisedToolExecutors = Array.isArray(runtimeOptions.toolExecutors)
878
+ ? runtimeOptions.toolExecutors.filter(isHubToolExecutorName)
879
+ : [];
880
+ const started = await this.sessionHost.start({
881
+ source: typeof metadata.source === "string" ? metadata.source : undefined,
882
+ interactive: metadata.interactive !== false,
883
+ sessionMetadata:
884
+ Object.keys(metadata as Record<string, unknown>).length > 0
885
+ ? (metadata as Record<string, unknown>)
886
+ : undefined,
887
+ initialMessages: Array.isArray(payload.initialMessages)
888
+ ? (payload.initialMessages as never[])
889
+ : undefined,
890
+ localRuntime: {
891
+ modelCatalogDefaults: {
892
+ loadLatestOnInit: true,
893
+ loadPrivateOnAuth: true,
894
+ },
895
+ defaultToolExecutors: createCapabilityBackedToolExecutors(
896
+ clientId,
897
+ advertisedToolExecutors,
898
+ async (
899
+ sessionId,
900
+ capabilityName,
901
+ capabilityPayload,
902
+ targetClientId,
903
+ ) =>
904
+ await this.requestCapability(
905
+ sessionId,
906
+ capabilityName,
907
+ capabilityPayload,
908
+ targetClientId,
909
+ ),
910
+ ),
911
+ },
912
+ requestToolApproval: async (request: ToolApprovalRequest) => {
913
+ return await this.requestToolApproval(request);
914
+ },
915
+ config: {
916
+ ...(sessionConfig ?? {}),
917
+ providerId:
918
+ sessionConfig?.providerId ??
919
+ (typeof modelSelection.provider === "string"
920
+ ? modelSelection.provider
921
+ : typeof metadata.provider === "string"
922
+ ? metadata.provider
923
+ : "hub"),
924
+ modelId:
925
+ sessionConfig?.modelId ??
926
+ (typeof modelSelection.model === "string"
927
+ ? modelSelection.model
928
+ : typeof metadata.model === "string"
929
+ ? metadata.model
930
+ : "hub"),
931
+ apiKey:
932
+ sessionConfig?.apiKey ??
933
+ (typeof modelSelection.apiKey === "string"
934
+ ? modelSelection.apiKey
935
+ : undefined),
936
+ cwd:
937
+ sessionConfig?.cwd ??
938
+ (typeof payload.cwd === "string" && payload.cwd.trim()
939
+ ? payload.cwd.trim()
940
+ : workspaceRoot),
941
+ workspaceRoot: sessionConfig?.workspaceRoot ?? workspaceRoot,
942
+ systemPrompt:
943
+ sessionConfig?.systemPrompt ??
944
+ (typeof runtimeOptions.systemPrompt === "string"
945
+ ? runtimeOptions.systemPrompt
946
+ : ""),
947
+ mode:
948
+ sessionConfig?.mode ??
949
+ (runtimeOptions.mode === "plan" || runtimeOptions.mode === "yolo"
950
+ ? runtimeOptions.mode
951
+ : "act"),
952
+ maxIterations:
953
+ sessionConfig?.maxIterations ??
954
+ (typeof runtimeOptions.maxIterations === "number"
955
+ ? runtimeOptions.maxIterations
956
+ : undefined),
957
+ enableTools:
958
+ sessionConfig?.enableTools ?? runtimeOptions.enableTools !== false,
959
+ enableSpawnAgent:
960
+ sessionConfig?.enableSpawnAgent ??
961
+ runtimeOptions.enableSpawn !== false,
962
+ enableAgentTeams:
963
+ sessionConfig?.enableAgentTeams ??
964
+ runtimeOptions.enableTeams !== false,
965
+ checkpoint:
966
+ sessionConfig?.checkpoint ??
967
+ (runtimeOptions.checkpointEnabled === true
968
+ ? { enabled: true }
969
+ : undefined),
970
+ teamName:
971
+ sessionConfig?.teamName ??
972
+ (typeof metadata.teamName === "string"
973
+ ? metadata.teamName
974
+ : undefined),
975
+ },
976
+ toolPolicies:
977
+ payload.toolPolicies &&
978
+ typeof payload.toolPolicies === "object" &&
979
+ !Array.isArray(payload.toolPolicies)
980
+ ? (JSON.parse(JSON.stringify(payload.toolPolicies)) as Record<
981
+ string,
982
+ { autoApprove?: boolean; enabled?: boolean }
983
+ >)
984
+ : runtimeOptions.autoApproveTools === true
985
+ ? { "*": { autoApprove: true } }
986
+ : undefined,
987
+ });
988
+ this.ensureSessionState(started.sessionId, clientId, "creator");
989
+ const session = await this.readHubSessionRecord(started.sessionId);
990
+ if (session) {
991
+ this.publish(
992
+ this.buildEvent("session.created", { session }, started.sessionId),
993
+ );
994
+ }
995
+ return {
996
+ version: envelope.version,
997
+ requestId: envelope.requestId,
998
+ ok: true,
999
+ payload: { session },
1000
+ };
1001
+ }
1002
+
1003
+ private async handleSessionAttach(
1004
+ envelope: HubCommandEnvelope,
1005
+ ): Promise<HubReplyEnvelope> {
1006
+ const sessionId =
1007
+ typeof envelope.payload?.sessionId === "string"
1008
+ ? envelope.payload.sessionId.trim()
1009
+ : envelope.sessionId?.trim() || "";
1010
+ if (!sessionId) {
1011
+ return {
1012
+ version: envelope.version,
1013
+ requestId: envelope.requestId,
1014
+ ok: false,
1015
+ error: {
1016
+ code: "invalid_session_attach",
1017
+ message: "session.attach requires a session id",
1018
+ },
1019
+ };
1020
+ }
1021
+ this.ensureSessionState(
1022
+ sessionId,
1023
+ envelope.clientId?.trim() || "hub-client",
1024
+ "participant",
1025
+ );
1026
+ const session = await this.readHubSessionRecord(sessionId);
1027
+ if (session) {
1028
+ this.publish(this.buildEvent("session.attached", { session }, sessionId));
1029
+ }
1030
+ return {
1031
+ version: envelope.version,
1032
+ requestId: envelope.requestId,
1033
+ ok: Boolean(session),
1034
+ ...(session
1035
+ ? { payload: { session } }
1036
+ : {
1037
+ error: {
1038
+ code: "session_not_found",
1039
+ message: `Unknown session: ${sessionId}`,
1040
+ },
1041
+ }),
1042
+ };
1043
+ }
1044
+
1045
+ private async handleSessionDetach(
1046
+ envelope: HubCommandEnvelope,
1047
+ ): Promise<HubReplyEnvelope> {
1048
+ const sessionId =
1049
+ typeof envelope.payload?.sessionId === "string"
1050
+ ? envelope.payload.sessionId.trim()
1051
+ : envelope.sessionId?.trim() || "";
1052
+ if (!sessionId) {
1053
+ return {
1054
+ version: envelope.version,
1055
+ requestId: envelope.requestId,
1056
+ ok: false,
1057
+ error: {
1058
+ code: "invalid_session_detach",
1059
+ message: "session.detach requires a session id",
1060
+ },
1061
+ };
1062
+ }
1063
+ const clientId = envelope.clientId?.trim() || "hub-client";
1064
+ const state = this.sessionState.get(sessionId);
1065
+ if (state) {
1066
+ state.participants.delete(clientId);
1067
+ if (state.participants.size === 0) {
1068
+ this.sessionState.delete(sessionId);
1069
+ }
1070
+ }
1071
+ const session = await this.readHubSessionRecord(sessionId);
1072
+ this.publish(
1073
+ this.buildEvent(
1074
+ "session.detached",
1075
+ session ? { session, clientId } : { clientId },
1076
+ sessionId,
1077
+ ),
1078
+ );
1079
+ return {
1080
+ version: envelope.version,
1081
+ requestId: envelope.requestId,
1082
+ ok: true,
1083
+ };
1084
+ }
1085
+
1086
+ private async handleSessionGet(
1087
+ envelope: HubCommandEnvelope,
1088
+ ): Promise<HubReplyEnvelope> {
1089
+ const sessionId =
1090
+ typeof envelope.payload?.sessionId === "string"
1091
+ ? envelope.payload.sessionId.trim()
1092
+ : envelope.sessionId?.trim() || "";
1093
+ const session = await this.readHubSessionRecord(sessionId);
1094
+ return {
1095
+ version: envelope.version,
1096
+ requestId: envelope.requestId,
1097
+ ok: Boolean(session),
1098
+ ...(session
1099
+ ? { payload: { session } }
1100
+ : {
1101
+ error: {
1102
+ code: "session_not_found",
1103
+ message: `Unknown session: ${sessionId}`,
1104
+ },
1105
+ }),
1106
+ };
1107
+ }
1108
+
1109
+ private async handleSessionList(
1110
+ envelope: HubCommandEnvelope,
1111
+ ): Promise<HubReplyEnvelope> {
1112
+ const limit =
1113
+ typeof envelope.payload?.limit === "number"
1114
+ ? envelope.payload.limit
1115
+ : 200;
1116
+ const sessions = (await this.sessionHost.list(limit)).map((session) =>
1117
+ toHubSessionRecord(session, this.sessionState.get(session.sessionId)),
1118
+ );
1119
+ return {
1120
+ version: envelope.version,
1121
+ requestId: envelope.requestId,
1122
+ ok: true,
1123
+ payload: { sessions },
1124
+ };
1125
+ }
1126
+
1127
+ private async handleSessionUpdate(
1128
+ envelope: HubCommandEnvelope,
1129
+ ): Promise<HubReplyEnvelope> {
1130
+ const sessionId =
1131
+ typeof envelope.payload?.sessionId === "string"
1132
+ ? envelope.payload.sessionId.trim()
1133
+ : envelope.sessionId?.trim() || "";
1134
+ const metadata =
1135
+ envelope.payload?.metadata &&
1136
+ typeof envelope.payload.metadata === "object" &&
1137
+ !Array.isArray(envelope.payload.metadata)
1138
+ ? (envelope.payload.metadata as Record<string, JsonValue | undefined>)
1139
+ : undefined;
1140
+ const updated = await this.sessionHost.update(sessionId, { metadata });
1141
+ const session = await this.readHubSessionRecord(sessionId);
1142
+ if (session) {
1143
+ this.publish(this.buildEvent("session.updated", { session }, sessionId));
1144
+ }
1145
+ return {
1146
+ version: envelope.version,
1147
+ requestId: envelope.requestId,
1148
+ ok: updated.updated,
1149
+ payload: { updated: updated.updated, session },
1150
+ };
1151
+ }
1152
+
1153
+ private async handleSessionDelete(
1154
+ envelope: HubCommandEnvelope,
1155
+ ): Promise<HubReplyEnvelope> {
1156
+ const sessionId =
1157
+ typeof envelope.payload?.sessionId === "string"
1158
+ ? envelope.payload.sessionId.trim()
1159
+ : envelope.sessionId?.trim() || "";
1160
+ const deleted = await this.sessionHost.delete(sessionId);
1161
+ this.sessionState.delete(sessionId);
1162
+ return {
1163
+ version: envelope.version,
1164
+ requestId: envelope.requestId,
1165
+ ok: true,
1166
+ payload: { deleted },
1167
+ };
1168
+ }
1169
+
1170
+ private async handleSessionInput(
1171
+ envelope: HubCommandEnvelope,
1172
+ ): Promise<HubReplyEnvelope> {
1173
+ const sessionId =
1174
+ typeof envelope.payload?.sessionId === "string"
1175
+ ? envelope.payload.sessionId.trim()
1176
+ : envelope.sessionId?.trim() || "";
1177
+ const payload =
1178
+ envelope.payload && typeof envelope.payload === "object"
1179
+ ? envelope.payload
1180
+ : {};
1181
+ const prompt =
1182
+ typeof payload.prompt === "string"
1183
+ ? payload.prompt
1184
+ : typeof payload.input === "string"
1185
+ ? payload.input
1186
+ : "";
1187
+ if (!prompt.trim()) {
1188
+ return {
1189
+ version: envelope.version,
1190
+ requestId: envelope.requestId,
1191
+ ok: false,
1192
+ error: {
1193
+ code: "invalid_session_input",
1194
+ message: "session input requires a prompt string",
1195
+ },
1196
+ };
1197
+ }
1198
+ this.publish(this.buildEvent("run.started", undefined, sessionId));
1199
+ const attachments =
1200
+ payload.attachments &&
1201
+ typeof payload.attachments === "object" &&
1202
+ !Array.isArray(payload.attachments)
1203
+ ? (payload.attachments as Record<string, unknown>)
1204
+ : undefined;
1205
+ const result = await this.sessionHost.send({
1206
+ sessionId,
1207
+ prompt,
1208
+ delivery:
1209
+ payload.delivery === "queue" || payload.delivery === "steer"
1210
+ ? payload.delivery
1211
+ : undefined,
1212
+ userImages: Array.isArray(attachments?.userImages)
1213
+ ? (attachments.userImages as string[])
1214
+ : undefined,
1215
+ });
1216
+ if (result) {
1217
+ this.publish(
1218
+ this.buildEvent(
1219
+ "run.completed",
1220
+ { reason: result.finishReason, result },
1221
+ sessionId,
1222
+ ),
1223
+ );
1224
+ }
1225
+ return {
1226
+ version: envelope.version,
1227
+ requestId: envelope.requestId,
1228
+ ok: true,
1229
+ payload: result ? { result } : undefined,
1230
+ };
1231
+ }
1232
+
1233
+ private async handleRunAbort(
1234
+ envelope: HubCommandEnvelope,
1235
+ ): Promise<HubReplyEnvelope> {
1236
+ const sessionId =
1237
+ typeof envelope.payload?.sessionId === "string"
1238
+ ? envelope.payload.sessionId.trim()
1239
+ : envelope.sessionId?.trim() || "";
1240
+ await this.sessionHost.abort(sessionId, envelope.payload?.reason);
1241
+ this.publish(
1242
+ this.buildEvent(
1243
+ "run.aborted",
1244
+ typeof envelope.payload?.reason === "string"
1245
+ ? { reason: envelope.payload.reason }
1246
+ : undefined,
1247
+ sessionId,
1248
+ ),
1249
+ );
1250
+ return {
1251
+ version: envelope.version,
1252
+ requestId: envelope.requestId,
1253
+ ok: true,
1254
+ payload: { applied: true },
1255
+ };
1256
+ }
1257
+
1258
+ private async handleSessionHook(
1259
+ envelope: HubCommandEnvelope,
1260
+ ): Promise<HubReplyEnvelope> {
1261
+ const parsed = parseHookEventPayload(envelope.payload?.payload);
1262
+ if (!parsed) {
1263
+ return {
1264
+ version: envelope.version,
1265
+ requestId: envelope.requestId,
1266
+ ok: false,
1267
+ error: {
1268
+ code: "invalid_hook_payload",
1269
+ message: "session.hook requires a valid hook event payload",
1270
+ },
1271
+ };
1272
+ }
1273
+ await this.sessionHost.handleHookEvent(parsed);
1274
+ return {
1275
+ version: envelope.version,
1276
+ requestId: envelope.requestId,
1277
+ ok: true,
1278
+ payload: { applied: true },
1279
+ };
1280
+ }
1281
+
1282
+ private async requestToolApproval(
1283
+ request: ToolApprovalRequest,
1284
+ ): Promise<{ approved: boolean; reason?: string }> {
1285
+ const approvalId = createSessionId("approval_");
1286
+ return await new Promise((resolve) => {
1287
+ this.pendingApprovals.set(approvalId, {
1288
+ sessionId: request.conversationId,
1289
+ resolve,
1290
+ });
1291
+ this.publish(
1292
+ this.buildEvent(
1293
+ "approval.requested",
1294
+ {
1295
+ approvalId,
1296
+ toolCallId: request.toolCallId,
1297
+ toolName: request.toolName,
1298
+ inputJson: JSON.stringify(request.input ?? null),
1299
+ },
1300
+ request.conversationId,
1301
+ ),
1302
+ );
1303
+ });
1304
+ }
1305
+
1306
+ private async handleApprovalRespond(
1307
+ envelope: HubCommandEnvelope,
1308
+ ): Promise<HubReplyEnvelope> {
1309
+ const approvalId =
1310
+ typeof envelope.payload?.approvalId === "string"
1311
+ ? envelope.payload.approvalId.trim()
1312
+ : "";
1313
+ const pending = this.pendingApprovals.get(approvalId);
1314
+ if (!pending) {
1315
+ return {
1316
+ version: envelope.version,
1317
+ requestId: envelope.requestId,
1318
+ ok: false,
1319
+ error: {
1320
+ code: "approval_not_found",
1321
+ message: `Unknown approval: ${approvalId}`,
1322
+ },
1323
+ };
1324
+ }
1325
+ this.pendingApprovals.delete(approvalId);
1326
+ const reason =
1327
+ envelope.payload?.payload &&
1328
+ typeof envelope.payload.payload === "object" &&
1329
+ !Array.isArray(envelope.payload.payload) &&
1330
+ typeof (envelope.payload.payload as Record<string, unknown>).reason ===
1331
+ "string"
1332
+ ? ((envelope.payload.payload as Record<string, unknown>)
1333
+ .reason as string)
1334
+ : undefined;
1335
+ pending.resolve({
1336
+ approved: envelope.payload?.approved === true,
1337
+ reason,
1338
+ });
1339
+ this.publish(
1340
+ this.buildEvent(
1341
+ "approval.resolved",
1342
+ { approvalId, approved: envelope.payload?.approved === true, reason },
1343
+ pending.sessionId,
1344
+ ),
1345
+ );
1346
+ return {
1347
+ version: envelope.version,
1348
+ requestId: envelope.requestId,
1349
+ ok: true,
1350
+ payload: { approvalId, approved: envelope.payload?.approved === true },
1351
+ };
1352
+ }
1353
+
1354
+ private async handleCapabilityRequest(
1355
+ envelope: HubCommandEnvelope,
1356
+ ): Promise<HubReplyEnvelope> {
1357
+ const sessionId =
1358
+ typeof envelope.payload?.sessionId === "string"
1359
+ ? envelope.payload.sessionId.trim()
1360
+ : envelope.sessionId?.trim() || "";
1361
+ const capabilityName =
1362
+ typeof envelope.payload?.capabilityName === "string"
1363
+ ? envelope.payload.capabilityName.trim()
1364
+ : "";
1365
+ const targetClientId =
1366
+ typeof envelope.payload?.targetClientId === "string"
1367
+ ? envelope.payload.targetClientId.trim()
1368
+ : "";
1369
+ if (!sessionId || !capabilityName || !targetClientId) {
1370
+ return {
1371
+ version: envelope.version,
1372
+ requestId: envelope.requestId,
1373
+ ok: false,
1374
+ error: {
1375
+ code: "invalid_capability_request",
1376
+ message:
1377
+ "capability.request requires sessionId, capabilityName, and targetClientId",
1378
+ },
1379
+ };
1380
+ }
1381
+ try {
1382
+ const payload =
1383
+ envelope.payload?.payload &&
1384
+ typeof envelope.payload.payload === "object" &&
1385
+ !Array.isArray(envelope.payload.payload)
1386
+ ? (envelope.payload.payload as Record<string, unknown>)
1387
+ : {};
1388
+ const response = await this.requestCapability(
1389
+ sessionId,
1390
+ capabilityName,
1391
+ payload,
1392
+ targetClientId,
1393
+ );
1394
+ return {
1395
+ version: envelope.version,
1396
+ requestId: envelope.requestId,
1397
+ ok: true,
1398
+ payload: response,
1399
+ };
1400
+ } catch (error) {
1401
+ return {
1402
+ version: envelope.version,
1403
+ requestId: envelope.requestId,
1404
+ ok: false,
1405
+ error: {
1406
+ code: "capability_request_failed",
1407
+ message: error instanceof Error ? error.message : String(error),
1408
+ },
1409
+ };
1410
+ }
1411
+ }
1412
+
1413
+ private async handleCapabilityRespond(
1414
+ envelope: HubCommandEnvelope,
1415
+ ): Promise<HubReplyEnvelope> {
1416
+ const requestId =
1417
+ typeof envelope.payload?.requestId === "string"
1418
+ ? envelope.payload.requestId.trim()
1419
+ : "";
1420
+ const pending = this.pendingCapabilityRequests.get(requestId);
1421
+ if (!pending) {
1422
+ return {
1423
+ version: envelope.version,
1424
+ requestId: envelope.requestId,
1425
+ ok: false,
1426
+ error: {
1427
+ code: "capability_not_found",
1428
+ message: `Unknown capability request: ${requestId}`,
1429
+ },
1430
+ };
1431
+ }
1432
+ this.pendingCapabilityRequests.delete(requestId);
1433
+ const payload =
1434
+ envelope.payload?.payload &&
1435
+ typeof envelope.payload.payload === "object" &&
1436
+ !Array.isArray(envelope.payload.payload)
1437
+ ? (envelope.payload.payload as Record<string, unknown>)
1438
+ : undefined;
1439
+ const error =
1440
+ typeof envelope.payload?.error === "string"
1441
+ ? envelope.payload.error
1442
+ : undefined;
1443
+ const ok = envelope.payload?.ok === true;
1444
+ pending.resolve({ ok, payload, error });
1445
+ this.publish(
1446
+ this.buildEvent(
1447
+ "capability.resolved",
1448
+ {
1449
+ requestId,
1450
+ capabilityName: pending.capabilityName,
1451
+ targetClientId: envelope.clientId?.trim(),
1452
+ ok,
1453
+ payload,
1454
+ error,
1455
+ },
1456
+ pending.sessionId,
1457
+ ),
1458
+ );
1459
+ return {
1460
+ version: envelope.version,
1461
+ requestId: envelope.requestId,
1462
+ ok: true,
1463
+ payload: { requestId, ok },
1464
+ };
1465
+ }
1466
+
1467
+ private async handleSessionEvent(event: CoreSessionEvent): Promise<void> {
1468
+ switch (event.type) {
1469
+ case "chunk":
1470
+ // Ignore raw agent chunks here. In this runtime they can contain
1471
+ // serialized event envelopes rather than user-facing assistant text.
1472
+ // Structured live content is forwarded via the "agent_event" branch.
1473
+ return;
1474
+ case "agent_event": {
1475
+ const { sessionId, event: agentEvent } = event.payload;
1476
+ if (agentEvent.type === "content_start") {
1477
+ if (
1478
+ agentEvent.contentType === "text" &&
1479
+ typeof agentEvent.text === "string" &&
1480
+ agentEvent.text.length > 0
1481
+ ) {
1482
+ this.publish(
1483
+ this.buildEvent(
1484
+ "assistant.delta",
1485
+ { text: agentEvent.text },
1486
+ sessionId,
1487
+ ),
1488
+ );
1489
+ return;
1490
+ }
1491
+ if (agentEvent.contentType === "reasoning") {
1492
+ if (agentEvent.redacted && !agentEvent.reasoning) {
1493
+ this.publish(
1494
+ this.buildEvent(
1495
+ "reasoning.delta",
1496
+ { text: "", redacted: true },
1497
+ sessionId,
1498
+ ),
1499
+ );
1500
+ return;
1501
+ }
1502
+ if (
1503
+ typeof agentEvent.reasoning === "string" &&
1504
+ agentEvent.reasoning.length > 0
1505
+ ) {
1506
+ this.publish(
1507
+ this.buildEvent(
1508
+ "reasoning.delta",
1509
+ {
1510
+ text: agentEvent.reasoning,
1511
+ redacted: agentEvent.redacted === true,
1512
+ },
1513
+ sessionId,
1514
+ ),
1515
+ );
1516
+ }
1517
+ return;
1518
+ }
1519
+ if (agentEvent.contentType === "tool") {
1520
+ this.publish(
1521
+ this.buildEvent(
1522
+ "tool.started",
1523
+ {
1524
+ toolCallId: agentEvent.toolCallId,
1525
+ toolName: agentEvent.toolName,
1526
+ input: agentEvent.input,
1527
+ },
1528
+ sessionId,
1529
+ ),
1530
+ );
1531
+ return;
1532
+ }
1533
+ }
1534
+ if (
1535
+ agentEvent.type === "content_end" &&
1536
+ agentEvent.contentType === "tool"
1537
+ ) {
1538
+ this.publish(
1539
+ this.buildEvent(
1540
+ "tool.finished",
1541
+ {
1542
+ toolCallId: agentEvent.toolCallId,
1543
+ toolName: agentEvent.toolName,
1544
+ output: agentEvent.output,
1545
+ error: agentEvent.error,
1546
+ },
1547
+ sessionId,
1548
+ ),
1549
+ );
1550
+ }
1551
+ return;
1552
+ }
1553
+ case "hook":
1554
+ if (event.payload.hookEventName === "tool_call") {
1555
+ this.publish(
1556
+ this.buildEvent(
1557
+ "tool.started",
1558
+ { toolName: event.payload.toolName },
1559
+ event.payload.sessionId,
1560
+ ),
1561
+ );
1562
+ } else if (event.payload.hookEventName === "tool_result") {
1563
+ this.publish(
1564
+ this.buildEvent(
1565
+ "tool.finished",
1566
+ { toolName: event.payload.toolName },
1567
+ event.payload.sessionId,
1568
+ ),
1569
+ );
1570
+ }
1571
+ return;
1572
+ case "team_progress": {
1573
+ const projection: TeamProgressProjectionEvent = {
1574
+ type: "team_progress_projection",
1575
+ version: 1,
1576
+ sessionId: event.payload.sessionId,
1577
+ summary: event.payload.summary,
1578
+ lastEvent: event.payload.lifecycle,
1579
+ };
1580
+ this.publish(
1581
+ this.buildEvent(
1582
+ "team.progress",
1583
+ projection as unknown as Record<string, unknown>,
1584
+ event.payload.sessionId,
1585
+ ),
1586
+ );
1587
+ return;
1588
+ }
1589
+ case "status": {
1590
+ const session = await this.readHubSessionRecord(
1591
+ event.payload.sessionId,
1592
+ );
1593
+ if (session) {
1594
+ this.publish(
1595
+ this.buildEvent(
1596
+ "session.updated",
1597
+ { session },
1598
+ event.payload.sessionId,
1599
+ ),
1600
+ );
1601
+ }
1602
+ return;
1603
+ }
1604
+ case "ended":
1605
+ if (event.payload.reason === "completed") {
1606
+ const session = await this.readHubSessionRecord(
1607
+ event.payload.sessionId,
1608
+ );
1609
+ const notification = await buildCompletionNotification(session);
1610
+ this.publish(
1611
+ this.buildEvent("ui.notify", notification, event.payload.sessionId),
1612
+ );
1613
+ }
1614
+ this.publish(
1615
+ this.buildEvent(
1616
+ event.payload.reason === "aborted"
1617
+ ? "run.aborted"
1618
+ : "run.completed",
1619
+ { reason: event.payload.reason },
1620
+ event.payload.sessionId,
1621
+ ),
1622
+ );
1623
+ return;
1624
+ default:
1625
+ return;
1626
+ }
1627
+ }
1628
+
1629
+ subscribe(
1630
+ clientId: string,
1631
+ listener: (event: HubEventEnvelope) => void,
1632
+ options?: { sessionId?: string },
1633
+ ): () => void {
1634
+ const current = this.listeners.get(clientId) ?? new Set();
1635
+ const entry = { sessionId: options?.sessionId, listener };
1636
+ current.add(entry);
1637
+ this.listeners.set(clientId, current);
1638
+ return () => {
1639
+ const listeners = this.listeners.get(clientId);
1640
+ if (!listeners) {
1641
+ return;
1642
+ }
1643
+ listeners.delete(entry);
1644
+ if (listeners.size === 0) {
1645
+ this.listeners.delete(clientId);
1646
+ }
1647
+ };
1648
+ }
1649
+
1650
+ private publish(event: HubEventEnvelope): void {
1651
+ for (const entries of this.listeners.values()) {
1652
+ for (const entry of entries) {
1653
+ if (entry.sessionId && entry.sessionId !== event.sessionId) {
1654
+ continue;
1655
+ }
1656
+ entry.listener(event);
1657
+ }
1658
+ }
1659
+ }
1660
+ }
1661
+
1662
+ export interface HubWebSocketServerOptions {
1663
+ host?: string;
1664
+ port?: number;
1665
+ pathname?: string;
1666
+ owner?: HubOwnerContext;
1667
+ sessionHost?: RuntimeHost;
1668
+ runtimeHandlers: HubScheduleRuntimeHandlers;
1669
+ scheduleOptions?: Omit<HubScheduleServiceOptions, "runtimeHandlers">;
1670
+ /**
1671
+ * Custom `fetch` implementation forwarded to the internally-constructed
1672
+ * `LocalRuntimeHost` that executes incoming `session.create` traffic.
1673
+ * Used by the AI gateway providers for every session that runs inside
1674
+ * this hub process.
1675
+ *
1676
+ * Ignored when `sessionHost` is supplied — in that case the caller owns
1677
+ * runtime construction and is responsible for wiring its own fetch.
1678
+ */
1679
+ fetch?: typeof fetch;
1680
+ }
1681
+
1682
+ export interface HubWebSocketServer {
1683
+ host: string;
1684
+ port: number;
1685
+ url: string;
1686
+ close(): Promise<void>;
1687
+ }
1688
+
1689
+ export interface EnsureHubWebSocketServerOptions
1690
+ extends HubWebSocketServerOptions {}
1691
+
1692
+ export interface EnsuredHubWebSocketServerResult {
1693
+ server?: HubWebSocketServer;
1694
+ url: string;
1695
+ action: "reuse" | "started";
1696
+ }
1697
+
1698
+ const SHARED_SERVERS = new Map<string, Promise<HubWebSocketServer>>();
1699
+
1700
+ export async function startHubWebSocketServer(
1701
+ options: HubWebSocketServerOptions,
1702
+ ): Promise<HubWebSocketServer> {
1703
+ const owner = options.owner ?? resolveHubOwnerContext();
1704
+ const host = options.host ?? "127.0.0.1";
1705
+ const pathname = options.pathname ?? "/hub";
1706
+ const requestedPort = options.port ?? 25463;
1707
+ let port = requestedPort;
1708
+ let url = createHubServerUrl(host, requestedPort, pathname);
1709
+ const buildId = resolveHubBuildId();
1710
+ const transport = new HubServerTransport(options);
1711
+ await transport.start();
1712
+ const adapter = new BrowserWebSocketHubAdapter(
1713
+ new NativeHubTransportAdapter(transport),
1714
+ );
1715
+ const cleanup = new Set<() => void>();
1716
+ const startedAt = new Date().toISOString();
1717
+ const versionPayload = {
1718
+ protocolVersion: "v1",
1719
+ buildId,
1720
+ pid: process.pid,
1721
+ startedAt,
1722
+ } as const;
1723
+
1724
+ const server = http.createServer((req, res) => {
1725
+ if ((req.url ?? "/") === "/health") {
1726
+ const body = JSON.stringify({
1727
+ hubId: transport.getHubId(),
1728
+ ...versionPayload,
1729
+ host,
1730
+ port,
1731
+ url,
1732
+ updatedAt: new Date().toISOString(),
1733
+ } satisfies HubServerDiscoveryRecord);
1734
+ res.statusCode = 200;
1735
+ res.setHeader("content-type", "application/json");
1736
+ res.end(body);
1737
+ return;
1738
+ }
1739
+ if ((req.url ?? "/") === "/version") {
1740
+ res.statusCode = 200;
1741
+ res.setHeader("content-type", "application/json");
1742
+ res.end(JSON.stringify(versionPayload));
1743
+ return;
1744
+ }
1745
+ res.statusCode = 404;
1746
+ res.end("Not found");
1747
+ });
1748
+ const wss = new WebSocketServer({ noServer: true });
1749
+
1750
+ server.on("upgrade", (request, socket, head) => {
1751
+ const requestUrl = new URL(request.url ?? "/", `http://${host}:${port}`);
1752
+ if (requestUrl.pathname !== pathname) {
1753
+ socket.destroy();
1754
+ return;
1755
+ }
1756
+ wss.handleUpgrade(request, socket, head, (websocket: NodeWebSocketLike) => {
1757
+ const detach = adapter.attach(wrapWsSocket(websocket));
1758
+ cleanup.add(detach);
1759
+ websocket.once("close", () => {
1760
+ detach();
1761
+ cleanup.delete(detach);
1762
+ });
1763
+ });
1764
+ });
1765
+
1766
+ await new Promise<void>((resolve, reject) => {
1767
+ server.once("error", (error) => {
1768
+ reject(
1769
+ formatHubStartupError(error, {
1770
+ host,
1771
+ port: requestedPort,
1772
+ pathname,
1773
+ }),
1774
+ );
1775
+ });
1776
+ server.listen(requestedPort, host, () => {
1777
+ const address = server.address();
1778
+ if (!address || typeof address === "string") {
1779
+ reject(
1780
+ formatHubStartupError(new Error("Failed to resolve hub port"), {
1781
+ host,
1782
+ port: requestedPort,
1783
+ pathname,
1784
+ }),
1785
+ );
1786
+ return;
1787
+ }
1788
+ port = address.port;
1789
+ url = createHubServerUrl(host, port, pathname);
1790
+ resolve();
1791
+ });
1792
+ });
1793
+
1794
+ await writeHubDiscovery(owner.discoveryPath, {
1795
+ hubId: transport.getHubId(),
1796
+ protocolVersion: "v1",
1797
+ buildId,
1798
+ host,
1799
+ port,
1800
+ url,
1801
+ pid: process.pid,
1802
+ startedAt,
1803
+ updatedAt: startedAt,
1804
+ });
1805
+
1806
+ return {
1807
+ host,
1808
+ port,
1809
+ url,
1810
+ close: async () => {
1811
+ for (const detach of cleanup) {
1812
+ detach();
1813
+ }
1814
+ cleanup.clear();
1815
+ await new Promise<void>((resolve, reject) => {
1816
+ wss.close((error?: Error) => {
1817
+ if (error) {
1818
+ reject(error);
1819
+ return;
1820
+ }
1821
+ resolve();
1822
+ });
1823
+ });
1824
+ await new Promise<void>((resolve, reject) => {
1825
+ server.close((error) => {
1826
+ if (error) {
1827
+ reject(error);
1828
+ return;
1829
+ }
1830
+ resolve();
1831
+ });
1832
+ });
1833
+ await transport.stop();
1834
+ const current = await readHubDiscovery(owner.discoveryPath);
1835
+ if (current?.url === url) {
1836
+ await clearHubDiscovery(owner.discoveryPath);
1837
+ }
1838
+ },
1839
+ };
1840
+ }
1841
+
1842
+ export async function ensureHubWebSocketServer(
1843
+ options: EnsureHubWebSocketServerOptions,
1844
+ ): Promise<EnsuredHubWebSocketServerResult> {
1845
+ const owner = options.owner ?? resolveHubOwnerContext();
1846
+ const host = options.host ?? "127.0.0.1";
1847
+ const port = options.port ?? 25463;
1848
+ const pathname = options.pathname ?? "/hub";
1849
+ const expectedUrl = createHubServerUrl(host, port, pathname);
1850
+ const sharedKey = owner.discoveryPath;
1851
+ const existing = SHARED_SERVERS.get(sharedKey);
1852
+ if (existing) {
1853
+ const server = await existing;
1854
+ if (server.url === expectedUrl) {
1855
+ return { server, url: server.url, action: "reuse" };
1856
+ }
1857
+ }
1858
+
1859
+ return await withHubStartupLock(owner.discoveryPath, async () => {
1860
+ const discovered = await readHubDiscovery(owner.discoveryPath);
1861
+ if (discovered?.url === expectedUrl) {
1862
+ const healthy = await probeHubServer(discovered.url);
1863
+ if (healthy?.url && (await verifyHubConnection(healthy.url))) {
1864
+ return { url: healthy.url, action: "reuse" };
1865
+ }
1866
+ }
1867
+
1868
+ const expected = await probeHubServer(expectedUrl);
1869
+ if (expected?.url && (await verifyHubConnection(expected.url))) {
1870
+ await writeHubDiscovery(owner.discoveryPath, expected);
1871
+ return { url: expected.url, action: "reuse" };
1872
+ }
1873
+
1874
+ if (discovered?.url) {
1875
+ await clearHubDiscovery(owner.discoveryPath);
1876
+ }
1877
+
1878
+ const serverPromise = startHubWebSocketServer({ ...options, owner });
1879
+ SHARED_SERVERS.set(sharedKey, serverPromise);
1880
+ try {
1881
+ const server = await serverPromise;
1882
+ return { server, url: server.url, action: "started" };
1883
+ } catch (error) {
1884
+ SHARED_SERVERS.delete(sharedKey);
1885
+ throw error;
1886
+ }
1887
+ });
1888
+ }