vibepulse 0.2.0 → 0.2.1

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 (420) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +4 -0
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/cache/.previewinfo +1 -1
  5. package/.next/cache/.rscinfo +1 -1
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/cache/config.json +3 -3
  8. package/.next/fallback-build-manifest.json +2 -2
  9. package/.next/prerender-manifest.json +3 -3
  10. package/.next/routes-manifest.json +32 -0
  11. package/.next/server/app/_global-error/page.js +1 -1
  12. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  13. package/.next/server/app/_global-error.html +2 -2
  14. package/.next/server/app/_global-error.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/_not-found/page.js +1 -1
  21. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/_not-found.html +1 -1
  24. package/.next/server/app/_not-found.rsc +2 -2
  25. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  26. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  27. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  28. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  29. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  30. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  31. package/.next/server/app/api/node/events/route.js +1 -1
  32. package/.next/server/app/api/node/events/route.js.nft.json +1 -1
  33. package/.next/server/app/api/node/health/route.js +1 -1
  34. package/.next/server/app/api/node/health/route.js.nft.json +1 -1
  35. package/.next/server/app/api/node/sessions/[id]/archive/route/app-paths-manifest.json +3 -0
  36. package/.next/server/app/api/node/sessions/[id]/archive/route/build-manifest.json +11 -0
  37. package/.next/server/app/api/node/sessions/[id]/archive/route/server-reference-manifest.json +4 -0
  38. package/.next/server/app/api/node/sessions/[id]/archive/route.js +6 -0
  39. package/.next/server/app/api/node/sessions/[id]/archive/route.js.map +5 -0
  40. package/.next/server/app/api/node/sessions/[id]/archive/route.js.nft.json +1 -0
  41. package/.next/server/app/api/node/sessions/[id]/archive/route_client-reference-manifest.js +2 -0
  42. package/.next/server/app/api/node/sessions/[id]/delete/route/app-paths-manifest.json +3 -0
  43. package/.next/server/app/api/node/sessions/[id]/delete/route/build-manifest.json +11 -0
  44. package/.next/server/app/api/node/sessions/[id]/delete/route/server-reference-manifest.json +4 -0
  45. package/.next/server/app/api/node/sessions/[id]/delete/route.js +7 -0
  46. package/.next/server/app/api/node/sessions/[id]/delete/route.js.map +5 -0
  47. package/.next/server/app/api/node/sessions/[id]/delete/route.js.nft.json +1 -0
  48. package/.next/server/app/api/node/sessions/[id]/delete/route_client-reference-manifest.js +2 -0
  49. package/.next/server/app/api/node/sessions/[id]/open-editor/route/app-paths-manifest.json +3 -0
  50. package/.next/server/app/api/node/sessions/[id]/open-editor/route/build-manifest.json +11 -0
  51. package/.next/server/app/api/node/sessions/[id]/open-editor/route/server-reference-manifest.json +4 -0
  52. package/.next/server/app/api/node/sessions/[id]/open-editor/route.js +7 -0
  53. package/.next/server/app/api/node/sessions/[id]/open-editor/route.js.map +5 -0
  54. package/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -0
  55. package/.next/server/app/api/node/sessions/[id]/open-editor/route_client-reference-manifest.js +2 -0
  56. package/.next/server/app/api/node/sessions/route.js +1 -1
  57. package/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
  58. package/.next/server/app/api/opencode-events/route.js +1 -1
  59. package/.next/server/app/api/opencode-events/route.js.nft.json +1 -1
  60. package/.next/server/app/api/sessions/[id]/archive/route.js +2 -1
  61. package/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
  63. package/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sessions/[id]/open-editor/route/app-paths-manifest.json +3 -0
  65. package/.next/server/app/api/sessions/[id]/open-editor/route/build-manifest.json +11 -0
  66. package/.next/server/app/api/sessions/[id]/open-editor/route/server-reference-manifest.json +4 -0
  67. package/.next/server/app/api/sessions/[id]/open-editor/route.js +7 -0
  68. package/.next/server/app/api/sessions/[id]/open-editor/route.js.map +5 -0
  69. package/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -0
  70. package/.next/server/app/api/sessions/[id]/open-editor/route_client-reference-manifest.js +2 -0
  71. package/.next/server/app/api/sessions/route.js +1 -1
  72. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  73. package/.next/server/app/index.html +1 -1
  74. package/.next/server/app/index.rsc +3 -3
  75. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  76. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  77. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  78. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  79. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  80. package/.next/server/app/page.js +1 -1
  81. package/.next/server/app/page.js.nft.json +1 -1
  82. package/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/server/app-paths-manifest.json +4 -0
  84. package/.next/server/chunks/[root-of-the-server]__1211da38._.js +3 -0
  85. package/.next/server/chunks/[root-of-the-server]__1211da38._.js.map +1 -0
  86. package/.next/{standalone/.next/server/chunks/[root-of-the-server]__b698889b._.js → server/chunks/[root-of-the-server]__1b87ec42._.js} +2 -2
  87. package/.next/server/chunks/[root-of-the-server]__1b87ec42._.js.map +1 -0
  88. package/.next/server/chunks/[root-of-the-server]__2b526e7a._.js +3 -0
  89. package/.next/server/chunks/[root-of-the-server]__2b526e7a._.js.map +1 -0
  90. package/.next/server/chunks/[root-of-the-server]__2f981540._.js +3 -0
  91. package/.next/server/chunks/[root-of-the-server]__2f981540._.js.map +1 -0
  92. package/.next/server/chunks/[root-of-the-server]__3745b314._.js +3 -0
  93. package/.next/server/chunks/[root-of-the-server]__3745b314._.js.map +1 -0
  94. package/.next/server/chunks/[root-of-the-server]__56690af0._.js +1 -1
  95. package/.next/server/chunks/[root-of-the-server]__56690af0._.js.map +1 -1
  96. package/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
  97. package/.next/server/chunks/[root-of-the-server]__56f5f249._.js.map +1 -1
  98. package/.next/server/chunks/[root-of-the-server]__59175de4._.js +1 -1
  99. package/.next/server/chunks/[root-of-the-server]__59175de4._.js.map +1 -1
  100. package/.next/server/chunks/[root-of-the-server]__64fffc02._.js +1 -1
  101. package/.next/server/chunks/[root-of-the-server]__64fffc02._.js.map +1 -1
  102. package/.next/server/chunks/[root-of-the-server]__6c428a24._.js +3 -0
  103. package/.next/server/chunks/[root-of-the-server]__6c428a24._.js.map +1 -0
  104. package/.next/server/chunks/[root-of-the-server]__73a00b88._.js +3 -0
  105. package/.next/server/chunks/[root-of-the-server]__73a00b88._.js.map +1 -0
  106. package/.next/server/chunks/[root-of-the-server]__89c5eeab._.js +1 -1
  107. package/.next/server/chunks/[root-of-the-server]__89c5eeab._.js.map +1 -1
  108. package/.next/server/chunks/[root-of-the-server]__8da6c5a8._.js +1 -1
  109. package/.next/server/chunks/[root-of-the-server]__8da6c5a8._.js.map +1 -1
  110. package/.next/server/chunks/[root-of-the-server]__b796d06c._.js +1 -1
  111. package/.next/server/chunks/[root-of-the-server]__b796d06c._.js.map +1 -1
  112. package/.next/server/chunks/[root-of-the-server]__c2ce5c0f._.js +1 -1
  113. package/.next/server/chunks/[root-of-the-server]__c2ce5c0f._.js.map +1 -1
  114. package/.next/server/chunks/[root-of-the-server]__d8e61048._.js +3 -0
  115. package/.next/server/chunks/[root-of-the-server]__d8e61048._.js.map +1 -0
  116. package/.next/server/chunks/[root-of-the-server]__db285678._.js +3 -0
  117. package/.next/server/chunks/[root-of-the-server]__db285678._.js.map +1 -0
  118. package/.next/{standalone/.next/server/chunks/[root-of-the-server]__16a9eb0a._.js → server/chunks/[root-of-the-server]__e00a9200._.js} +2 -2
  119. package/.next/server/chunks/{[root-of-the-server]__16a9eb0a._.js.map → [root-of-the-server]__e00a9200._.js.map} +1 -1
  120. package/.next/server/chunks/[root-of-the-server]__e5df5e5f._.js +3 -0
  121. package/.next/server/chunks/[root-of-the-server]__e5df5e5f._.js.map +1 -0
  122. package/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_archive_route_actions_237255a5.js +3 -0
  123. package/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_archive_route_actions_237255a5.js.map +1 -0
  124. package/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_delete_route_actions_e5d426f6.js +3 -0
  125. package/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_delete_route_actions_e5d426f6.js.map +1 -0
  126. package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_open-editor_route_actions_eaebf476.js +3 -0
  127. package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_open-editor_route_actions_eaebf476.js.map +1 -0
  128. package/.next/server/chunks/ce889_server_app_api_node_sessions_[id]_open-editor_route_actions_791cdf5b.js +3 -0
  129. package/.next/server/chunks/ce889_server_app_api_node_sessions_[id]_open-editor_route_actions_791cdf5b.js.map +1 -0
  130. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7e181e75.js +1 -1
  131. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7e181e75.js.map +1 -1
  132. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
  133. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js.map +1 -1
  134. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_fa835ac3.js +2 -2
  135. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_fa835ac3.js.map +1 -1
  136. package/.next/server/chunks/ssr/{[root-of-the-server]__efc52f08._.js → [root-of-the-server]__631e12d0._.js} +2 -2
  137. package/.next/server/chunks/ssr/[root-of-the-server]__a8cd3911._.js +3 -0
  138. package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
  139. package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js.map +1 -1
  140. package/.next/server/pages/404.html +1 -1
  141. package/.next/server/pages/500.html +2 -2
  142. package/.next/server/server-reference-manifest.js +1 -1
  143. package/.next/server/server-reference-manifest.json +1 -1
  144. package/.next/standalone/.next/BUILD_ID +1 -1
  145. package/.next/standalone/.next/app-path-routes-manifest.json +4 -0
  146. package/.next/standalone/.next/build-manifest.json +2 -2
  147. package/.next/standalone/.next/prerender-manifest.json +3 -3
  148. package/.next/standalone/.next/routes-manifest.json +32 -0
  149. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  150. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  151. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  152. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  153. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  154. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  155. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  156. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  157. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  158. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  159. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  160. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  161. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  162. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  163. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  164. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  165. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  166. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  167. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  168. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  169. package/.next/standalone/.next/server/app/api/node/events/route.js +1 -1
  170. package/.next/standalone/.next/server/app/api/node/events/route.js.nft.json +1 -1
  171. package/.next/standalone/.next/server/app/api/node/health/route.js +1 -1
  172. package/.next/standalone/.next/server/app/api/node/health/route.js.nft.json +1 -1
  173. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route/app-paths-manifest.json +3 -0
  174. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route/build-manifest.json +11 -0
  175. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route/server-reference-manifest.json +4 -0
  176. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route.js +6 -0
  177. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route.js.map +5 -0
  178. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route.js.nft.json +1 -0
  179. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route_client-reference-manifest.js +2 -0
  180. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route/app-paths-manifest.json +3 -0
  181. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route/build-manifest.json +11 -0
  182. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route/server-reference-manifest.json +4 -0
  183. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route.js +7 -0
  184. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route.js.map +5 -0
  185. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route.js.nft.json +1 -0
  186. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route_client-reference-manifest.js +2 -0
  187. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route/app-paths-manifest.json +3 -0
  188. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route/build-manifest.json +11 -0
  189. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route/server-reference-manifest.json +4 -0
  190. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js +7 -0
  191. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js.map +5 -0
  192. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -0
  193. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route_client-reference-manifest.js +2 -0
  194. package/.next/standalone/.next/server/app/api/node/sessions/route.js +1 -1
  195. package/.next/standalone/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
  196. package/.next/standalone/.next/server/app/api/opencode-events/route.js +1 -1
  197. package/.next/standalone/.next/server/app/api/opencode-events/route.js.nft.json +1 -1
  198. package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js +2 -1
  199. package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  200. package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
  201. package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
  202. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route/app-paths-manifest.json +3 -0
  203. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route/build-manifest.json +11 -0
  204. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route/server-reference-manifest.json +4 -0
  205. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js +7 -0
  206. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js.map +5 -0
  207. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -0
  208. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route_client-reference-manifest.js +2 -0
  209. package/.next/standalone/.next/server/app/api/sessions/route.js +1 -1
  210. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  211. package/.next/standalone/.next/server/app/index.html +1 -1
  212. package/.next/standalone/.next/server/app/index.rsc +3 -3
  213. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  214. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  215. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  216. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  217. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  218. package/.next/standalone/.next/server/app/page.js +1 -1
  219. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  220. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  221. package/.next/standalone/.next/server/app-paths-manifest.json +4 -0
  222. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1211da38._.js +3 -0
  223. package/.next/{server/chunks/[root-of-the-server]__b698889b._.js → standalone/.next/server/chunks/[root-of-the-server]__1b87ec42._.js} +2 -2
  224. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2b526e7a._.js +3 -0
  225. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2f981540._.js +3 -0
  226. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3745b314._.js +3 -0
  227. package/.next/standalone/.next/server/chunks/[root-of-the-server]__56690af0._.js +1 -1
  228. package/.next/standalone/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
  229. package/.next/standalone/.next/server/chunks/[root-of-the-server]__59175de4._.js +1 -1
  230. package/.next/standalone/.next/server/chunks/[root-of-the-server]__64fffc02._.js +1 -1
  231. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c428a24._.js +3 -0
  232. package/.next/standalone/.next/server/chunks/[root-of-the-server]__73a00b88._.js +3 -0
  233. package/.next/standalone/.next/server/chunks/[root-of-the-server]__89c5eeab._.js +1 -1
  234. package/.next/standalone/.next/server/chunks/[root-of-the-server]__8da6c5a8._.js +1 -1
  235. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b796d06c._.js +1 -1
  236. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c2ce5c0f._.js +1 -1
  237. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8e61048._.js +3 -0
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__db285678._.js +3 -0
  239. package/.next/{server/chunks/[root-of-the-server]__16a9eb0a._.js → standalone/.next/server/chunks/[root-of-the-server]__e00a9200._.js} +2 -2
  240. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e5df5e5f._.js +3 -0
  241. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_archive_route_actions_237255a5.js +3 -0
  242. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_delete_route_actions_e5d426f6.js +3 -0
  243. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_open-editor_route_actions_eaebf476.js +3 -0
  244. package/.next/standalone/.next/server/chunks/ce889_server_app_api_node_sessions_[id]_open-editor_route_actions_791cdf5b.js +3 -0
  245. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7e181e75.js +1 -1
  246. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
  247. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_fa835ac3.js +2 -2
  248. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__efc52f08._.js → [root-of-the-server]__631e12d0._.js} +2 -2
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__a8cd3911._.js +3 -0
  250. package/.next/standalone/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
  251. package/.next/standalone/.next/server/pages/404.html +1 -1
  252. package/.next/standalone/.next/server/pages/500.html +2 -2
  253. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  254. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  255. package/.next/standalone/.next/static/chunks/7ac19aaef01f4a03.js +13 -0
  256. package/.next/standalone/.next/static/chunks/f42202943f6742e5.css +3 -0
  257. package/.next/standalone/AGENTS.md +85 -0
  258. package/.next/standalone/README.md +76 -0
  259. package/.next/standalone/__mocks__/child_process.ts +4 -0
  260. package/.next/standalone/bin/dev-runtime.js +58 -0
  261. package/.next/standalone/bin/vibepulse.js +87 -0
  262. package/.next/standalone/check-hsql.mjs +71 -0
  263. package/.next/standalone/docs/session-status-detection.md +258 -0
  264. package/.next/standalone/eslint.config.mjs +31 -0
  265. package/.next/standalone/next.config.ts +8 -0
  266. package/.next/standalone/package-lock.json +10312 -0
  267. package/.next/standalone/package.json +1 -1
  268. package/.next/standalone/postcss.config.mjs +7 -0
  269. package/.next/standalone/src/AGENTS.md +41 -0
  270. package/.next/standalone/src/app/api/AGENTS.md +40 -0
  271. package/.next/standalone/src/app/api/node/events/route.test.ts +196 -0
  272. package/.next/standalone/src/app/api/node/events/route.ts +259 -0
  273. package/.next/standalone/src/app/api/node/health/route.test.ts +190 -0
  274. package/.next/standalone/src/app/api/node/health/route.ts +48 -0
  275. package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.test.ts +128 -0
  276. package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.ts +97 -0
  277. package/.next/standalone/src/app/api/node/sessions/[id]/delete/route.test.ts +113 -0
  278. package/.next/standalone/src/app/api/node/sessions/[id]/delete/route.ts +81 -0
  279. package/.next/standalone/src/app/api/node/sessions/[id]/open-editor/route.test.ts +206 -0
  280. package/.next/standalone/src/app/api/node/sessions/[id]/open-editor/route.ts +123 -0
  281. package/.next/standalone/src/app/api/node/sessions/route.test.ts +408 -0
  282. package/.next/standalone/src/app/api/node/sessions/route.ts +1094 -0
  283. package/.next/standalone/src/app/api/nodes/route.test.ts +237 -0
  284. package/.next/standalone/src/app/api/nodes/route.ts +176 -0
  285. package/.next/standalone/src/app/api/opencode-config/route.test.ts +86 -0
  286. package/.next/standalone/src/app/api/opencode-config/route.ts +376 -0
  287. package/.next/standalone/src/app/api/opencode-config/status/route.ts +31 -0
  288. package/.next/standalone/src/app/api/opencode-events/route.test.ts +624 -0
  289. package/.next/standalone/src/app/api/opencode-events/route.ts +508 -0
  290. package/.next/standalone/src/app/api/opencode-models/route.test.ts +167 -0
  291. package/.next/standalone/src/app/api/opencode-models/route.ts +76 -0
  292. package/.next/standalone/src/app/api/profiles/[id]/apply/route.ts +49 -0
  293. package/.next/standalone/src/app/api/profiles/[id]/export/route.ts +31 -0
  294. package/.next/standalone/src/app/api/profiles/[id]/route.ts +160 -0
  295. package/.next/standalone/src/app/api/profiles/import/route.test.js +107 -0
  296. package/.next/standalone/src/app/api/profiles/import/route.ts +65 -0
  297. package/.next/standalone/src/app/api/profiles/route.ts +107 -0
  298. package/.next/standalone/src/app/api/sessions/[id]/archive/route.test.ts +136 -0
  299. package/.next/standalone/src/app/api/sessions/[id]/archive/route.ts +170 -0
  300. package/.next/standalone/src/app/api/sessions/[id]/delete/route.test.ts +113 -0
  301. package/.next/standalone/src/app/api/sessions/[id]/delete/route.ts +137 -0
  302. package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.test.ts +218 -0
  303. package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.ts +85 -0
  304. package/.next/standalone/src/app/api/sessions/[id]/route.test.ts +531 -0
  305. package/.next/standalone/src/app/api/sessions/[id]/route.ts +75 -0
  306. package/.next/standalone/src/app/api/sessions/route.test.ts +1298 -0
  307. package/.next/standalone/src/app/api/sessions/route.ts +1695 -0
  308. package/.next/standalone/src/app/favicon.ico +0 -0
  309. package/.next/standalone/src/app/globals.css +66 -0
  310. package/.next/standalone/src/app/layout.tsx +37 -0
  311. package/.next/standalone/src/app/page.test.tsx +134 -0
  312. package/.next/standalone/src/app/page.tsx +358 -0
  313. package/.next/standalone/src/components/AGENTS.md +42 -0
  314. package/.next/standalone/src/components/ErrorBoundary.tsx +72 -0
  315. package/.next/standalone/src/components/KanbanBoard.test.tsx +704 -0
  316. package/.next/standalone/src/components/KanbanBoard.tsx +852 -0
  317. package/.next/standalone/src/components/LoadingState.tsx +37 -0
  318. package/.next/standalone/src/components/ProjectCard.test.tsx +773 -0
  319. package/.next/standalone/src/components/ProjectCard.tsx +595 -0
  320. package/.next/standalone/src/components/QueryProvider.tsx +25 -0
  321. package/.next/standalone/src/components/SessionCard.test.tsx +566 -0
  322. package/.next/standalone/src/components/SessionCard.tsx +434 -0
  323. package/.next/standalone/src/components/SessionList.tsx +60 -0
  324. package/.next/standalone/src/components/host-config/HostManagerDialog.test.tsx +252 -0
  325. package/.next/standalone/src/components/host-config/HostManagerDialog.tsx +476 -0
  326. package/.next/standalone/src/components/opencode-config/AgentConfigForm.test.tsx +72 -0
  327. package/.next/standalone/src/components/opencode-config/AgentConfigForm.tsx +483 -0
  328. package/.next/standalone/src/components/opencode-config/AgentModelSelector.tsx +284 -0
  329. package/.next/standalone/src/components/opencode-config/AgentsConfigPanel.tsx +162 -0
  330. package/.next/standalone/src/components/opencode-config/ConfigButton.tsx +43 -0
  331. package/.next/standalone/src/components/opencode-config/ConfigPanel.tsx +91 -0
  332. package/.next/standalone/src/components/opencode-config/FullscreenConfigPanel.tsx +435 -0
  333. package/.next/standalone/src/components/opencode-config/GeneralSettingsForm.test.tsx +91 -0
  334. package/.next/standalone/src/components/opencode-config/GeneralSettingsForm.tsx +288 -0
  335. package/.next/standalone/src/components/opencode-config/categories/CategoriesList.tsx +382 -0
  336. package/.next/standalone/src/components/opencode-config/categories/CategoriesManager.test.tsx +111 -0
  337. package/.next/standalone/src/components/opencode-config/categories/CategoriesManager.tsx +174 -0
  338. package/.next/standalone/src/components/opencode-config/categories/CategoryConfigForm.tsx +453 -0
  339. package/.next/standalone/src/components/opencode-config/profiles/ProfileCard.tsx +140 -0
  340. package/.next/standalone/src/components/opencode-config/profiles/ProfileEditor.tsx +446 -0
  341. package/.next/standalone/src/components/opencode-config/profiles/ProfileList.tsx +446 -0
  342. package/.next/standalone/src/components/opencode-config/profiles/ProfileManager.test.tsx +225 -0
  343. package/.next/standalone/src/components/opencode-config/profiles/ProfileManager.tsx +405 -0
  344. package/.next/standalone/src/components/ui/Tabs.tsx +59 -0
  345. package/.next/standalone/src/hooks/useHostSources.test.ts +509 -0
  346. package/.next/standalone/src/hooks/useHostSources.ts +299 -0
  347. package/.next/standalone/src/hooks/useOpencodeSync.test.ts +387 -0
  348. package/.next/standalone/src/hooks/useOpencodeSync.ts +571 -0
  349. package/.next/standalone/src/index.ts +2 -0
  350. package/.next/standalone/src/lib/editorLauncher.server.ts +36 -0
  351. package/.next/standalone/src/lib/editorLauncher.test.ts +35 -0
  352. package/.next/standalone/src/lib/editorLauncher.ts +25 -0
  353. package/.next/standalone/src/lib/hostAccent.test.ts +58 -0
  354. package/.next/standalone/src/lib/hostAccent.ts +46 -0
  355. package/.next/standalone/src/lib/hostIdentity.test.ts +187 -0
  356. package/.next/standalone/src/lib/hostIdentity.ts +122 -0
  357. package/.next/standalone/src/lib/hostSourcesStorage.test.ts +141 -0
  358. package/.next/standalone/src/lib/hostSourcesStorage.ts +72 -0
  359. package/.next/standalone/src/lib/nodeProtocol.test.ts +159 -0
  360. package/.next/standalone/src/lib/nodeProtocol.ts +142 -0
  361. package/.next/standalone/src/lib/nodeRegistry.test.ts +173 -0
  362. package/.next/standalone/src/lib/nodeRegistry.ts +398 -0
  363. package/.next/standalone/src/lib/notificationSound.ts +292 -0
  364. package/.next/standalone/src/lib/opencodeConfig.test.ts +100 -0
  365. package/.next/standalone/src/lib/opencodeConfig.ts +76 -0
  366. package/.next/standalone/src/lib/opencodeDiscovery.ts +275 -0
  367. package/.next/standalone/src/lib/profiles/share.test.ts +91 -0
  368. package/.next/standalone/src/lib/profiles/share.ts +93 -0
  369. package/.next/standalone/src/lib/profiles/storage.test.ts +108 -0
  370. package/.next/standalone/src/lib/profiles/storage.ts +370 -0
  371. package/.next/standalone/src/lib/runtimeMode.test.ts +29 -0
  372. package/.next/standalone/src/lib/runtimeMode.ts +29 -0
  373. package/.next/standalone/src/lib/sessionActionErrors.ts +37 -0
  374. package/.next/standalone/src/lib/sessionArchiveOverrides.test.ts +43 -0
  375. package/.next/standalone/src/lib/sessionArchiveOverrides.ts +116 -0
  376. package/.next/standalone/src/lib/transform.test.ts +121 -0
  377. package/.next/standalone/src/lib/transform.ts +193 -0
  378. package/.next/standalone/src/test/setup.ts +8 -0
  379. package/.next/standalone/src/types/index.ts +152 -0
  380. package/.next/standalone/src/types/opencodeConfig.ts +149 -0
  381. package/.next/standalone/tsconfig.json +34 -0
  382. package/.next/standalone/tsconfig.lib.json +17 -0
  383. package/.next/standalone/vitest.config.ts +16 -0
  384. package/.next/static/chunks/7ac19aaef01f4a03.js +13 -0
  385. package/.next/static/chunks/f42202943f6742e5.css +3 -0
  386. package/.next/trace +1 -1
  387. package/.next/trace-build +1 -1
  388. package/.next/types/routes.d.ts +5 -1
  389. package/.next/types/validator.ts +36 -0
  390. package/package.json +1 -1
  391. package/.next/server/chunks/[root-of-the-server]__0b017945._.js +0 -3
  392. package/.next/server/chunks/[root-of-the-server]__0b017945._.js.map +0 -1
  393. package/.next/server/chunks/[root-of-the-server]__1e118bd3._.js +0 -3
  394. package/.next/server/chunks/[root-of-the-server]__1e118bd3._.js.map +0 -1
  395. package/.next/server/chunks/[root-of-the-server]__6979e732._.js +0 -3
  396. package/.next/server/chunks/[root-of-the-server]__6979e732._.js.map +0 -1
  397. package/.next/server/chunks/[root-of-the-server]__a7b4d79d._.js +0 -3
  398. package/.next/server/chunks/[root-of-the-server]__a7b4d79d._.js.map +0 -1
  399. package/.next/server/chunks/[root-of-the-server]__b698889b._.js.map +0 -1
  400. package/.next/server/chunks/[root-of-the-server]__ddc251b7._.js +0 -3
  401. package/.next/server/chunks/[root-of-the-server]__ddc251b7._.js.map +0 -1
  402. package/.next/server/chunks/ssr/[root-of-the-server]__b0788643._.js +0 -3
  403. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b017945._.js +0 -3
  404. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1e118bd3._.js +0 -3
  405. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6979e732._.js +0 -3
  406. package/.next/standalone/.next/server/chunks/[root-of-the-server]__a7b4d79d._.js +0 -3
  407. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ddc251b7._.js +0 -3
  408. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__b0788643._.js +0 -3
  409. package/.next/standalone/.next/static/chunks/9f8c22002e7395e8.js +0 -13
  410. package/.next/standalone/.next/static/chunks/f1b55db60e7ed6f3.css +0 -3
  411. package/.next/static/chunks/9f8c22002e7395e8.js +0 -13
  412. package/.next/static/chunks/f1b55db60e7ed6f3.css +0 -3
  413. /package/.next/server/chunks/ssr/{[root-of-the-server]__efc52f08._.js.map → [root-of-the-server]__631e12d0._.js.map} +0 -0
  414. /package/.next/server/chunks/ssr/{[root-of-the-server]__b0788643._.js.map → [root-of-the-server]__a8cd3911._.js.map} +0 -0
  415. /package/.next/standalone/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_buildManifest.js +0 -0
  416. /package/.next/standalone/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_clientMiddlewareManifest.json +0 -0
  417. /package/.next/standalone/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_ssgManifest.js +0 -0
  418. /package/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_buildManifest.js +0 -0
  419. /package/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_clientMiddlewareManifest.json +0 -0
  420. /package/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_ssgManifest.js +0 -0
@@ -0,0 +1,1695 @@
1
+ import { createOpencodeClient } from '@opencode-ai/sdk';
2
+ import { execSync } from 'child_process';
3
+ import path from 'path';
4
+ import {
5
+ discoverOpencodePortsWithMeta,
6
+ discoverOpencodeProcessCwdsWithoutPortWithMeta,
7
+ } from '@/lib/opencodeDiscovery';
8
+ import { readConfig } from '@/lib/opencodeConfig';
9
+ import {
10
+ clearSessionForceUnarchived,
11
+ markSessionForceUnarchived,
12
+ pruneSessionStickyStatusBlocked,
13
+ pruneSessionForceUnarchived,
14
+ shouldForceSessionUnarchived,
15
+ takeSessionStickyStatusBlocked,
16
+ } from '@/lib/sessionArchiveOverrides';
17
+ import { composeSourceKey, parseSourceKey } from '@/lib/hostIdentity';
18
+ import { createNodeRequestHeaders, NODE_PROTOCOL_VERSION } from '@/lib/nodeProtocol';
19
+ import { listNodeRecords, type StoredNodeRecord } from '@/lib/nodeRegistry';
20
+ import { RUNTIME_ROLE_ENV_VAR } from '@/lib/runtimeMode';
21
+ import type { BuiltInHostSource, RemoteHostConfig } from '@/types';
22
+
23
+ type SessionLike = {
24
+ id: string;
25
+ slug?: string;
26
+ title?: string;
27
+ directory: string;
28
+ debugReason?: string;
29
+ parentID?: string;
30
+ time?: {
31
+ created: number;
32
+ updated: number;
33
+ archived?: number;
34
+ };
35
+ };
36
+
37
+ const CHILD_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
38
+ const CHILD_UNKNOWN_STATE_BUSY_WINDOW_MS = 2 * 60 * 1000;
39
+ const CHILD_STATUS_MESSAGE_CHECK_LIMIT = 50;
40
+ const STALL_DETECTION_WINDOW_MS = 30 * 1000;
41
+ const STATUS_STICKY_RETENTION_MS = 24 * 60 * 60 * 1000;
42
+ const STATUS_STICKY_ABSENT_RETENTION_MS = 30 * 60 * 1000;
43
+ const DEFAULT_STATUS_STICKY_MAX_ENTRIES = 5000;
44
+ const GIT_COMMAND_TIMEOUT_MS = 1200;
45
+ const sessionListTimeoutMs = readPositiveTimeoutEnv('OPENCODE_SESSIONS_LIST_TIMEOUT_MS', 6000);
46
+ const sessionStatusTimeoutMs = readPositiveTimeoutEnv('OPENCODE_SESSIONS_STATUS_TIMEOUT_MS', 4000);
47
+ const sessionMessagesTimeoutMs = readPositiveTimeoutEnv('OPENCODE_SESSIONS_MESSAGES_TIMEOUT_MS', 2500);
48
+ const nodeSessionsTimeoutMs = readPositiveTimeoutEnv('VIBEPULSE_NODE_SESSIONS_TIMEOUT_MS', 6000);
49
+
50
+ type StableRealtimeStatus = 'idle' | 'busy' | 'retry';
51
+
52
+ type StatusStickyState = {
53
+ lastBusyAt: number;
54
+ lastSeenAt: number;
55
+ };
56
+
57
+ const statusStickyState = new Map<string, StatusStickyState>();
58
+
59
+ function clearStickyStatusState(sessionId: string): void {
60
+ statusStickyState.delete(sessionId);
61
+ statusStickyState.delete(`child:${sessionId}`);
62
+ }
63
+
64
+ type ChildEntry = HostAwareFields & {
65
+ id: string;
66
+ slug?: string;
67
+ title?: string;
68
+ directory?: string;
69
+ debugReason?: string;
70
+ parentID?: string;
71
+ time?: { created: number; updated: number; archived?: number };
72
+ realTimeStatus: string;
73
+ waitingForUser: boolean;
74
+ };
75
+
76
+ type EnrichedSession = SessionLike & HostAwareFields & {
77
+ projectName: string;
78
+ branch: string | null;
79
+ realTimeStatus: 'idle' | 'busy' | 'retry';
80
+ waitingForUser: boolean;
81
+ children: ChildEntry[];
82
+ };
83
+
84
+ type SessionStatusStabilizationTarget = {
85
+ id: string;
86
+ time?: {
87
+ archived?: number;
88
+ };
89
+ realTimeStatus: string;
90
+ waitingForUser: boolean;
91
+ children: Array<{
92
+ id: string;
93
+ time?: {
94
+ archived?: number;
95
+ };
96
+ realTimeStatus: string;
97
+ waitingForUser: boolean;
98
+ }>;
99
+ };
100
+
101
+ type ProcessHint = {
102
+ pid: number;
103
+ directory: string;
104
+ projectName: string;
105
+ reason: 'process_without_api_port';
106
+ };
107
+
108
+ type SessionSource = BuiltInHostSource | (RemoteHostConfig & { hostKind: 'remote' });
109
+
110
+ type HostAwareFields = {
111
+ hostId?: string;
112
+ hostLabel?: string;
113
+ hostKind?: SessionSource['hostKind'];
114
+ hostBaseUrl?: string;
115
+ rawSessionId?: string;
116
+ sourceSessionKey?: string;
117
+ readOnly?: boolean;
118
+ };
119
+
120
+ type SessionHostStatus = {
121
+ hostId: string;
122
+ hostLabel: string;
123
+ hostKind: SessionSource['hostKind'];
124
+ online: boolean;
125
+ degraded?: boolean;
126
+ reason?: string;
127
+ baseUrl?: string;
128
+ };
129
+
130
+ type SourceResultMeta = {
131
+ online: boolean;
132
+ degraded?: boolean;
133
+ reason?: string;
134
+ };
135
+
136
+ type SessionsSuccessPayload = {
137
+ sessions: EnrichedSession[];
138
+ processHints: ProcessHint[];
139
+ failedPorts?: Array<{ port: number; reason: string }>;
140
+ degraded?: boolean;
141
+ hosts?: SessionHostStatus[];
142
+ hostStatuses?: SessionHostStatus[];
143
+ };
144
+
145
+ type SessionsRouteResult = {
146
+ payload: SessionsSuccessPayload | Record<string, unknown>;
147
+ status?: number;
148
+ sourceMeta?: SourceResultMeta;
149
+ };
150
+
151
+ const LOCAL_SOURCE: BuiltInHostSource = {
152
+ hostId: 'local',
153
+ hostLabel: 'Local',
154
+ hostKind: 'local',
155
+ };
156
+
157
+ export const dynamic = 'force-dynamic';
158
+
159
+ export async function GET() {
160
+ return handleGet();
161
+ }
162
+
163
+ export async function POST(request: Request) {
164
+ return handlePost(request);
165
+ }
166
+
167
+ type MessageStateStatus = string;
168
+
169
+ type MessagePart = {
170
+ state?: {
171
+ status?: unknown;
172
+ };
173
+ };
174
+
175
+ function readPositiveTimeoutEnv(name: string, fallback: number): number {
176
+ const raw = process.env[name];
177
+ const parsed = Number(raw);
178
+ if (Number.isFinite(parsed) && parsed > 0) {
179
+ return Math.floor(parsed);
180
+ }
181
+ return fallback;
182
+ }
183
+
184
+ function withTimeout<T>(operation: (signal: AbortSignal) => Promise<T>, timeoutMs: number, label: string): Promise<T> {
185
+ const timeoutError = new Error(`${label} timed out after ${timeoutMs}ms`);
186
+ const timeoutController = new AbortController();
187
+ let timeoutHandle: NodeJS.Timeout | undefined;
188
+ const timeoutPromise = new Promise<never>((_, reject) => {
189
+ timeoutHandle = setTimeout(() => {
190
+ timeoutController.abort();
191
+ reject(timeoutError);
192
+ }, timeoutMs);
193
+ });
194
+
195
+ const operationPromise = operation(timeoutController.signal).catch((error) => {
196
+ if (timeoutController.signal.aborted) {
197
+ throw timeoutError;
198
+ }
199
+
200
+ throw error;
201
+ });
202
+
203
+ return Promise.race([operationPromise, timeoutPromise]).finally(() => {
204
+ if (timeoutHandle) {
205
+ clearTimeout(timeoutHandle);
206
+ }
207
+ });
208
+ }
209
+
210
+ const WAITING_PART_STATUSES = new Set<string>([
211
+ 'awaiting-input',
212
+ 'awaiting_input',
213
+ 'input-required',
214
+ 'input_required',
215
+ 'requires-input',
216
+ 'requires_input',
217
+ 'blocked',
218
+ 'paused',
219
+ ]);
220
+
221
+ function normalizePartStatus(status: string): string {
222
+ return status.trim().toLowerCase();
223
+ }
224
+
225
+ function isWaitingPartStatus(status: string): boolean {
226
+ return WAITING_PART_STATUSES.has(normalizePartStatus(status));
227
+ }
228
+
229
+ function collectPartStatuses(messages: Array<{ parts?: MessagePart[] }>): MessageStateStatus[] {
230
+ const partStatuses: MessageStateStatus[] = [];
231
+
232
+ for (const message of messages) {
233
+ for (const part of message.parts || []) {
234
+ const status = part?.state?.status;
235
+ if (typeof status === 'string') {
236
+ const normalized = normalizePartStatus(status);
237
+ if (normalized) {
238
+ partStatuses.push(normalized);
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ return partStatuses;
245
+ }
246
+
247
+ async function fetchPartStatuses(
248
+ client: ReturnType<typeof createOpencodeClient>,
249
+ sessionId: string,
250
+ timeoutMs: number
251
+ ): Promise<MessageStateStatus[]> {
252
+ const messagesResult = await withTimeout(
253
+ (signal) =>
254
+ client.session.messages({
255
+ path: { id: sessionId },
256
+ query: { limit: 8 },
257
+ signal,
258
+ }),
259
+ timeoutMs,
260
+ `session.messages(${sessionId})`
261
+ );
262
+ const messages = (messagesResult.data || []) as Array<{ parts?: MessagePart[] }>;
263
+ return collectPartStatuses(messages);
264
+ }
265
+
266
+ function getUpdatedAt(session: { time?: { updated?: number; created?: number } }): number {
267
+ return session.time?.updated || session.time?.created || 0;
268
+ }
269
+
270
+ function normalizeRealtimeStatus(value: string | undefined): StableRealtimeStatus {
271
+ if (value === 'busy' || value === 'retry') return value;
272
+ return 'idle';
273
+ }
274
+
275
+ export function applyStickyBusyStatus(id: string, status: StableRealtimeStatus, now: number, stickyBusyWindowMs: number): StableRealtimeStatus {
276
+ const existing = statusStickyState.get(id) ?? { lastBusyAt: 0, lastSeenAt: now };
277
+
278
+ if (status === 'busy') {
279
+ existing.lastBusyAt = now;
280
+ existing.lastSeenAt = now;
281
+ statusStickyState.set(id, existing);
282
+ return status;
283
+ }
284
+
285
+ if (status === 'retry') {
286
+ existing.lastSeenAt = now;
287
+ statusStickyState.set(id, existing);
288
+ return status;
289
+ }
290
+
291
+ const shouldKeepBusy = existing.lastBusyAt > 0 && now - existing.lastBusyAt <= stickyBusyWindowMs;
292
+ existing.lastSeenAt = now;
293
+ statusStickyState.set(id, existing);
294
+ return shouldKeepBusy ? 'busy' : 'idle';
295
+ }
296
+
297
+ function getStickyStateMaxEntries(): number {
298
+ const raw = Number(process.env.OPENCODE_STATUS_STICKY_MAX_ENTRIES);
299
+ if (Number.isFinite(raw) && raw > 0) {
300
+ return Math.floor(raw);
301
+ }
302
+ return DEFAULT_STATUS_STICKY_MAX_ENTRIES;
303
+ }
304
+
305
+ function pruneStickyState(now: number, activeIds: Set<string>): void {
306
+ for (const [id, state] of statusStickyState) {
307
+ const ageMs = now - state.lastSeenAt;
308
+ const isActive = activeIds.has(id);
309
+ if (ageMs > STATUS_STICKY_RETENTION_MS || (!isActive && ageMs > STATUS_STICKY_ABSENT_RETENTION_MS)) {
310
+ statusStickyState.delete(id);
311
+ }
312
+ }
313
+
314
+ const maxEntries = getStickyStateMaxEntries();
315
+ if (statusStickyState.size <= maxEntries) {
316
+ return;
317
+ }
318
+
319
+ const overflow = statusStickyState.size - maxEntries;
320
+ const sortedByLastSeen = Array.from(statusStickyState.entries()).sort((a, b) => a[1].lastSeenAt - b[1].lastSeenAt);
321
+
322
+ let removed = 0;
323
+ for (const [id] of sortedByLastSeen) {
324
+ if (removed >= overflow) break;
325
+ if (activeIds.has(id)) continue;
326
+ statusStickyState.delete(id);
327
+ removed++;
328
+ }
329
+
330
+ if (removed >= overflow) {
331
+ return;
332
+ }
333
+
334
+ for (const [id] of sortedByLastSeen) {
335
+ if (removed >= overflow) break;
336
+ if (!statusStickyState.has(id)) continue;
337
+ statusStickyState.delete(id);
338
+ removed++;
339
+ }
340
+ }
341
+
342
+ function hasRecentActivity(session: { time?: { updated?: number } }, now: number): boolean {
343
+ const updatedAt = session.time?.updated;
344
+ if (!updatedAt) return false;
345
+ return now - updatedAt <= STALL_DETECTION_WINDOW_MS;
346
+ }
347
+
348
+ function toChildEntry(
349
+ child: SessionLike,
350
+ status: 'idle' | 'busy' | 'retry',
351
+ waitingForUser = false
352
+ ): ChildEntry {
353
+ return {
354
+ id: child.id,
355
+ slug: child.slug,
356
+ title: child.title,
357
+ directory: child.directory,
358
+ debugReason: child.debugReason,
359
+ parentID: child.parentID,
360
+ time: child.time,
361
+ realTimeStatus: status,
362
+ waitingForUser,
363
+ };
364
+ }
365
+
366
+ function clearSessionStabilizationState(session: SessionStatusStabilizationTarget): void {
367
+ clearStickyStatusState(session.id);
368
+ clearSessionForceUnarchived(session.id);
369
+ for (const child of session.children) {
370
+ clearStickyStatusState(`child:${child.id}`);
371
+ clearSessionForceUnarchived(child.id);
372
+ }
373
+ }
374
+
375
+ export function shouldSkipSessionStatusStabilization(
376
+ session: SessionStatusStabilizationTarget,
377
+ now: number
378
+ ): boolean {
379
+ if (takeSessionStickyStatusBlocked(session.id, now)) {
380
+ clearSessionStabilizationState(session);
381
+ return true;
382
+ }
383
+
384
+ if (session.time?.archived) {
385
+ clearSessionStabilizationState(session);
386
+ return true;
387
+ }
388
+
389
+ return false;
390
+ }
391
+
392
+ export function applyStickyStatusStabilization(
393
+ session: SessionStatusStabilizationTarget,
394
+ stickyNow: number,
395
+ stickyBusyDelayMs: number
396
+ ): void {
397
+ for (const child of session.children) {
398
+ if (child.time?.archived) {
399
+ clearStickyStatusState(`child:${child.id}`);
400
+ clearSessionForceUnarchived(child.id);
401
+ continue;
402
+ }
403
+
404
+ const normalizedChildStatus = normalizeRealtimeStatus(child.realTimeStatus);
405
+ const childStatusForStabilization =
406
+ child.waitingForUser && normalizedChildStatus === 'idle' ? 'retry' : normalizedChildStatus;
407
+ child.realTimeStatus = applyStickyBusyStatus(
408
+ `child:${child.id}`,
409
+ childStatusForStabilization,
410
+ stickyNow,
411
+ stickyBusyDelayMs
412
+ );
413
+
414
+ if (child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry' || child.waitingForUser) {
415
+ markSessionForceUnarchived(child.id, stickyNow);
416
+ }
417
+ }
418
+
419
+ const normalizedSessionStatus = normalizeRealtimeStatus(session.realTimeStatus);
420
+ const sessionStatusForStabilization =
421
+ session.waitingForUser && normalizedSessionStatus === 'idle' ? 'retry' : normalizedSessionStatus;
422
+ session.realTimeStatus = applyStickyBusyStatus(
423
+ session.id,
424
+ sessionStatusForStabilization,
425
+ stickyNow,
426
+ stickyBusyDelayMs
427
+ );
428
+
429
+ const hasActiveChildren = session.children.some(
430
+ (child) => child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry' || child.waitingForUser
431
+ );
432
+ const shouldAutoUnarchive =
433
+ session.realTimeStatus === 'busy' ||
434
+ session.realTimeStatus === 'retry' ||
435
+ session.waitingForUser ||
436
+ hasActiveChildren;
437
+
438
+ if (shouldAutoUnarchive) {
439
+ markSessionForceUnarchived(session.id, stickyNow);
440
+ }
441
+ }
442
+ // Get project name from directory path
443
+ function getProjectName(directory: string): string {
444
+ return path.basename(directory);
445
+ }
446
+
447
+ // Check if directory is a git repository
448
+ function isGitRepo(directory: string): boolean {
449
+ try {
450
+ const result = execSync('git rev-parse --is-inside-work-tree', {
451
+ cwd: directory,
452
+ encoding: 'utf-8',
453
+ stdio: ['ignore', 'pipe', 'ignore'],
454
+ timeout: GIT_COMMAND_TIMEOUT_MS,
455
+ });
456
+ return result.trim() === 'true';
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+
462
+ // Get git branch name
463
+ function getGitBranch(directory: string): string | null {
464
+ if (!isGitRepo(directory)) return null;
465
+ try {
466
+ const branch = execSync('git branch --show-current', {
467
+ cwd: directory,
468
+ encoding: 'utf-8',
469
+ stdio: ['ignore', 'pipe', 'ignore'],
470
+ timeout: GIT_COMMAND_TIMEOUT_MS,
471
+ });
472
+ return branch.trim() || null;
473
+ } catch {
474
+ return null;
475
+ }
476
+ }
477
+
478
+ async function readStickyBusyDelayMs(): Promise<number> {
479
+ let stickyBusyDelayMs = 1000; // default 1s
480
+ try {
481
+ const config = await readConfig();
482
+ const vibepulseRaw = config.vibepulse && typeof config.vibepulse === 'object' && !Array.isArray(config.vibepulse)
483
+ ? config.vibepulse
484
+ : {};
485
+ const vibepulse = vibepulseRaw as Record<string, unknown>;
486
+ const stickyDelay = vibepulse['stickyBusyDelayMs'] as number | undefined;
487
+ if (typeof stickyDelay === 'number' && Number.isFinite(stickyDelay) && stickyDelay >= 0) {
488
+ stickyBusyDelayMs = stickyDelay;
489
+ }
490
+ } catch {
491
+ // Use default if config read fails
492
+ }
493
+
494
+ return stickyBusyDelayMs;
495
+ }
496
+
497
+ function isRecord(value: unknown): value is Record<string, unknown> {
498
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
499
+ }
500
+
501
+ function isRemoteSource(source: SessionSource): source is RemoteHostConfig & { hostKind: 'remote' } {
502
+ return source.hostKind === 'remote';
503
+ }
504
+
505
+ function normalizeNodeBaseUrl(baseUrl: string): string | null {
506
+ const trimmed = baseUrl.trim();
507
+ if (!trimmed) {
508
+ return null;
509
+ }
510
+
511
+ try {
512
+ const parsed = new URL(trimmed);
513
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
514
+ return null;
515
+ }
516
+ if (parsed.username || parsed.password) {
517
+ return null;
518
+ }
519
+ parsed.hash = '';
520
+ return parsed.toString().replace(/\/+$/, '');
521
+ } catch {
522
+ return null;
523
+ }
524
+ }
525
+
526
+ function isNodeStatus(value: unknown): value is 'idle' | 'busy' | 'retry' {
527
+ return value === 'idle' || value === 'busy' || value === 'retry';
528
+ }
529
+
530
+ function isProcessHintValue(value: unknown): value is ProcessHint {
531
+ if (!isRecord(value)) {
532
+ return false;
533
+ }
534
+
535
+ return (
536
+ typeof value['pid'] === 'number' &&
537
+ typeof value['directory'] === 'string' &&
538
+ typeof value['projectName'] === 'string' &&
539
+ value['reason'] === 'process_without_api_port'
540
+ );
541
+ }
542
+
543
+ function isTimeValue(value: unknown): boolean {
544
+ if (!isRecord(value)) {
545
+ return false;
546
+ }
547
+
548
+ const created = value['created'];
549
+ const updated = value['updated'];
550
+ const archived = value['archived'];
551
+ return (
552
+ typeof created === 'number' &&
553
+ typeof updated === 'number' &&
554
+ (archived === undefined || typeof archived === 'number')
555
+ );
556
+ }
557
+
558
+ function isChildEntryValue(value: unknown): value is ChildEntry {
559
+ if (!isRecord(value)) {
560
+ return false;
561
+ }
562
+
563
+ if (typeof value['id'] !== 'string') {
564
+ return false;
565
+ }
566
+ if (!isNodeStatus(value['realTimeStatus'])) {
567
+ return false;
568
+ }
569
+ if (typeof value['waitingForUser'] !== 'boolean') {
570
+ return false;
571
+ }
572
+
573
+ const parentID = value['parentID'];
574
+ const directory = value['directory'];
575
+ const time = value['time'];
576
+ if (parentID !== undefined && typeof parentID !== 'string') {
577
+ return false;
578
+ }
579
+ if (directory !== undefined && typeof directory !== 'string') {
580
+ return false;
581
+ }
582
+ if (time !== undefined && !isTimeValue(time)) {
583
+ return false;
584
+ }
585
+
586
+ return true;
587
+ }
588
+
589
+ function isSessionValue(value: unknown): value is EnrichedSession {
590
+ if (!isRecord(value)) {
591
+ return false;
592
+ }
593
+
594
+ if (typeof value['id'] !== 'string') {
595
+ return false;
596
+ }
597
+ if (typeof value['directory'] !== 'string') {
598
+ return false;
599
+ }
600
+ if (typeof value['projectName'] !== 'string') {
601
+ return false;
602
+ }
603
+ const branch = value['branch'];
604
+ if (branch !== null && branch !== undefined && typeof branch !== 'string') {
605
+ return false;
606
+ }
607
+ if (!isNodeStatus(value['realTimeStatus'])) {
608
+ return false;
609
+ }
610
+ if (typeof value['waitingForUser'] !== 'boolean') {
611
+ return false;
612
+ }
613
+
614
+ const children = value['children'];
615
+ if (!Array.isArray(children) || children.some((child) => !isChildEntryValue(child))) {
616
+ return false;
617
+ }
618
+
619
+ const time = value['time'];
620
+ if (time !== undefined && !isTimeValue(time)) {
621
+ return false;
622
+ }
623
+
624
+ return true;
625
+ }
626
+
627
+ function parseRemoteNodeSessionsSuccessPayload(
628
+ body: unknown
629
+ ): { sessions: EnrichedSession[]; processHints: ProcessHint[]; degraded: boolean } | null {
630
+ if (!isRecord(body)) {
631
+ return null;
632
+ }
633
+
634
+ if (body['ok'] !== true || body['role'] !== 'node' || body['protocolVersion'] !== NODE_PROTOCOL_VERSION) {
635
+ return null;
636
+ }
637
+
638
+ const source = body['source'];
639
+ if (
640
+ !isRecord(source) ||
641
+ source['hostId'] !== 'local' ||
642
+ source['hostLabel'] !== 'Local' ||
643
+ source['hostKind'] !== 'local'
644
+ ) {
645
+ return null;
646
+ }
647
+
648
+ const upstream = body['upstream'];
649
+ if (!isRecord(upstream) || upstream['kind'] !== 'opencode' || typeof upstream['reachable'] !== 'boolean') {
650
+ return null;
651
+ }
652
+
653
+ const sessions = body['sessions'];
654
+ const processHints = body['processHints'];
655
+
656
+ if (!Array.isArray(sessions) || sessions.some((session) => !isSessionValue(session))) {
657
+ return null;
658
+ }
659
+ if (!Array.isArray(processHints) || processHints.some((hint) => !isProcessHintValue(hint))) {
660
+ return null;
661
+ }
662
+
663
+ return {
664
+ sessions: sessions as EnrichedSession[],
665
+ processHints: processHints as ProcessHint[],
666
+ degraded: body['degraded'] === true,
667
+ };
668
+ }
669
+
670
+ function parseSource(value: unknown): SessionSource | null {
671
+ if (!isRecord(value)) {
672
+ return null;
673
+ }
674
+
675
+ const hostId = typeof value['hostId'] === 'string' ? value['hostId'].trim() : value['hostId'];
676
+ const hostLabel = typeof value['hostLabel'] === 'string' ? value['hostLabel'].trim() : value['hostLabel'];
677
+ const hostKind = value['hostKind'];
678
+
679
+ if (hostId === LOCAL_SOURCE.hostId && hostLabel === LOCAL_SOURCE.hostLabel && hostKind === LOCAL_SOURCE.hostKind) {
680
+ return LOCAL_SOURCE;
681
+ }
682
+
683
+ const baseUrl = value['baseUrl'];
684
+ const enabled = value['enabled'];
685
+
686
+ if (
687
+ typeof hostId !== 'string' ||
688
+ typeof hostLabel !== 'string' ||
689
+ hostKind !== 'remote' ||
690
+ typeof baseUrl !== 'string' ||
691
+ typeof enabled !== 'boolean'
692
+ ) {
693
+ return null;
694
+ }
695
+
696
+ const normalizedBaseUrl = normalizeNodeBaseUrl(baseUrl);
697
+ if (!normalizedBaseUrl) {
698
+ return null;
699
+ }
700
+
701
+ return {
702
+ hostId,
703
+ hostLabel,
704
+ hostKind,
705
+ baseUrl: normalizedBaseUrl,
706
+ enabled,
707
+ };
708
+ }
709
+
710
+ function parseRequestedSources(body: unknown): SessionSource[] {
711
+ if (!isRecord(body) || !Array.isArray(body['sources']) || body['sources'].length === 0) {
712
+ throw new Error('Invalid sources payload');
713
+ }
714
+
715
+ const sources = body['sources'].map(parseSource);
716
+ if (sources.some((source) => source === null)) {
717
+ throw new Error('Invalid sources payload');
718
+ }
719
+
720
+ return sources as SessionSource[];
721
+ }
722
+
723
+ function toRouteResponse(result: SessionsRouteResult): Response {
724
+ if (result.status) {
725
+ return Response.json(result.payload, { status: result.status });
726
+ }
727
+
728
+ return Response.json(result.payload);
729
+ }
730
+
731
+ function toHostStatus(source: SessionSource, meta: SourceResultMeta): SessionHostStatus {
732
+ return {
733
+ hostId: source.hostId,
734
+ hostLabel: source.hostLabel,
735
+ hostKind: source.hostKind,
736
+ online: meta.online,
737
+ ...(meta.degraded ? { degraded: true } : {}),
738
+ ...(meta.reason ? { reason: meta.reason } : {}),
739
+ ...(isRemoteSource(source) ? { baseUrl: source.baseUrl } : {}),
740
+ };
741
+ }
742
+
743
+ function withHostAliases(payload: Record<string, unknown>, hostStatuses: SessionHostStatus[]): Record<string, unknown> {
744
+ return {
745
+ ...payload,
746
+ hosts: hostStatuses,
747
+ hostStatuses,
748
+ };
749
+ }
750
+
751
+ function readSuccessPayload(result: SessionsRouteResult): SessionsSuccessPayload {
752
+ if (!isRecord(result.payload)) {
753
+ return { sessions: [], processHints: [] };
754
+ }
755
+
756
+ const sessions = Array.isArray(result.payload['sessions'])
757
+ ? (result.payload['sessions'] as EnrichedSession[])
758
+ : [];
759
+ const processHints = Array.isArray(result.payload['processHints'])
760
+ ? (result.payload['processHints'] as ProcessHint[])
761
+ : [];
762
+ const failedPorts = Array.isArray(result.payload['failedPorts'])
763
+ ? (result.payload['failedPorts'] as Array<{ port: number; reason: string }>)
764
+ : undefined;
765
+ const degraded = result.payload['degraded'] === true;
766
+
767
+ return {
768
+ sessions,
769
+ processHints,
770
+ ...(failedPorts ? { failedPorts } : {}),
771
+ ...(degraded ? { degraded: true } : {}),
772
+ };
773
+ }
774
+
775
+ function toRawSessionId(value: string): string {
776
+ if (!value.includes(':')) {
777
+ return value;
778
+ }
779
+
780
+ try {
781
+ return parseSourceKey(value).sessionId;
782
+ } catch {
783
+ return value;
784
+ }
785
+ }
786
+
787
+ function composeSourceKeySafely(hostId: string, sessionId: string): string | undefined {
788
+ try {
789
+ return composeSourceKey(hostId, sessionId);
790
+ } catch {
791
+ return undefined;
792
+ }
793
+ }
794
+
795
+ function addHostMetadataToChildEntry(child: ChildEntry, source: SessionSource): ChildEntry | null {
796
+ const rawSessionId = child.rawSessionId ?? toRawSessionId(child.id);
797
+ const rawParentId = child.parentID ? toRawSessionId(child.parentID) : child.parentID;
798
+ const sourceSessionKey = composeSourceKeySafely(source.hostId, rawSessionId);
799
+ if (!sourceSessionKey) {
800
+ return null;
801
+ }
802
+
803
+ const sourceParentKey = rawParentId
804
+ ? (composeSourceKeySafely(source.hostId, rawParentId) ?? undefined)
805
+ : undefined;
806
+
807
+ return {
808
+ ...child,
809
+ id: sourceSessionKey,
810
+ parentID: sourceParentKey,
811
+ hostId: source.hostId,
812
+ hostLabel: source.hostLabel,
813
+ hostKind: source.hostKind,
814
+ ...(isRemoteSource(source) ? { hostBaseUrl: source.baseUrl } : {}),
815
+ rawSessionId,
816
+ sourceSessionKey,
817
+ readOnly: false,
818
+ };
819
+ }
820
+
821
+ function addHostMetadataToSession(session: EnrichedSession, source: SessionSource): EnrichedSession | null {
822
+ const rawSessionId = session.rawSessionId ?? toRawSessionId(session.id);
823
+ const rawParentId = session.parentID ? toRawSessionId(session.parentID) : session.parentID;
824
+ const sourceSessionKey = composeSourceKeySafely(source.hostId, rawSessionId);
825
+ if (!sourceSessionKey) {
826
+ return null;
827
+ }
828
+
829
+ const sourceParentKey = rawParentId
830
+ ? (composeSourceKeySafely(source.hostId, rawParentId) ?? undefined)
831
+ : undefined;
832
+ const children: ChildEntry[] = [];
833
+ for (const child of session.children) {
834
+ const enrichedChild = addHostMetadataToChildEntry(child, source);
835
+ if (enrichedChild) {
836
+ children.push(enrichedChild);
837
+ }
838
+ }
839
+
840
+ return {
841
+ ...session,
842
+ id: sourceSessionKey,
843
+ parentID: sourceParentKey,
844
+ hostId: source.hostId,
845
+ hostLabel: source.hostLabel,
846
+ hostKind: source.hostKind,
847
+ ...(isRemoteSource(source) ? { hostBaseUrl: source.baseUrl } : {}),
848
+ rawSessionId,
849
+ sourceSessionKey,
850
+ readOnly: false,
851
+ children,
852
+ };
853
+ }
854
+
855
+ function addHostMetadataToPayload(payload: Record<string, unknown>, source: SessionSource): Record<string, unknown> {
856
+ if (!Array.isArray(payload['sessions'])) {
857
+ return payload;
858
+ }
859
+
860
+ const sessions: EnrichedSession[] = [];
861
+ let droppedSessions = 0;
862
+ for (const session of payload['sessions'] as EnrichedSession[]) {
863
+ const enrichedSession = addHostMetadataToSession(session, source);
864
+ if (enrichedSession) {
865
+ sessions.push(enrichedSession);
866
+ continue;
867
+ }
868
+
869
+ droppedSessions += 1;
870
+ }
871
+
872
+ const payloadDegraded = payload['degraded'] === true;
873
+
874
+ return {
875
+ ...payload,
876
+ sessions,
877
+ ...(payloadDegraded || droppedSessions > 0 ? { degraded: true } : {}),
878
+ };
879
+ }
880
+
881
+ function sortChildEntries(children: ChildEntry[]): void {
882
+ children.sort((a, b) => {
883
+ const aActive = a.realTimeStatus === 'busy' || a.realTimeStatus === 'retry';
884
+ const bActive = b.realTimeStatus === 'busy' || b.realTimeStatus === 'retry';
885
+
886
+ if (aActive && !bActive) return -1;
887
+ if (!aActive && bActive) return 1;
888
+
889
+ const aTime = a.time?.updated || a.time?.created || 0;
890
+ const bTime = b.time?.updated || b.time?.created || 0;
891
+ return bTime - aTime;
892
+ });
893
+ }
894
+
895
+ function toErrorMessage(error: unknown): string {
896
+ return error instanceof Error ? error.message : String(error);
897
+ }
898
+
899
+ async function readJsonResponseBody(response: Response): Promise<unknown> {
900
+ try {
901
+ return await response.json();
902
+ } catch {
903
+ return null;
904
+ }
905
+ }
906
+
907
+ async function fetchNodeSessionsWithTimeout(
908
+ source: RemoteHostConfig & { hostKind: 'remote' },
909
+ nodeRecord: StoredNodeRecord
910
+ ): Promise<Response> {
911
+ const timeoutLabel = `node.sessions(${source.hostId})`;
912
+ const abortController = new AbortController();
913
+ let timedOut = false;
914
+ const timeoutHandle = setTimeout(() => {
915
+ timedOut = true;
916
+ abortController.abort();
917
+ }, nodeSessionsTimeoutMs);
918
+
919
+ try {
920
+ return await fetch(`${nodeRecord.baseUrl}/api/node/sessions`, {
921
+ method: 'GET',
922
+ headers: createNodeRequestHeaders(nodeRecord.token),
923
+ signal: abortController.signal,
924
+ });
925
+ } catch (error) {
926
+ if (timedOut) {
927
+ throw new Error(`${timeoutLabel} timed out after ${nodeSessionsTimeoutMs}ms`);
928
+ }
929
+
930
+ throw error;
931
+ } finally {
932
+ clearTimeout(timeoutHandle);
933
+ }
934
+ }
935
+
936
+ async function getRemoteNodeSessionsResult(
937
+ source: RemoteHostConfig & { hostKind: 'remote' },
938
+ nodeRecord: StoredNodeRecord | undefined
939
+ ): Promise<SessionsRouteResult> {
940
+ if (!nodeRecord) {
941
+ return {
942
+ payload: { sessions: [], processHints: [], degraded: true },
943
+ sourceMeta: {
944
+ online: false,
945
+ degraded: true,
946
+ reason: 'node_not_configured',
947
+ },
948
+ };
949
+ }
950
+
951
+ if (!nodeRecord.enabled) {
952
+ return {
953
+ payload: { sessions: [], processHints: [], degraded: true },
954
+ sourceMeta: {
955
+ online: false,
956
+ degraded: true,
957
+ reason: 'node_disabled',
958
+ },
959
+ };
960
+ }
961
+
962
+ try {
963
+ const response = await fetchNodeSessionsWithTimeout(source, nodeRecord);
964
+ const body = await readJsonResponseBody(response);
965
+
966
+ if (!response.ok) {
967
+ const reason =
968
+ isRecord(body) && typeof body['reason'] === 'string'
969
+ ? body['reason']
970
+ : `node_request_failed_${response.status}`;
971
+
972
+ return {
973
+ payload: { sessions: [], processHints: [], degraded: true },
974
+ sourceMeta: {
975
+ online: true,
976
+ degraded: true,
977
+ reason,
978
+ },
979
+ };
980
+ }
981
+
982
+ const successPayload = parseRemoteNodeSessionsSuccessPayload(body);
983
+ if (!successPayload) {
984
+ return {
985
+ payload: { sessions: [], processHints: [], degraded: true },
986
+ sourceMeta: {
987
+ online: true,
988
+ degraded: true,
989
+ reason: 'node_payload_invalid',
990
+ },
991
+ };
992
+ }
993
+
994
+ return {
995
+ payload: {
996
+ sessions: successPayload.sessions,
997
+ processHints: successPayload.processHints,
998
+ ...(successPayload.degraded ? { degraded: true } : {}),
999
+ },
1000
+ sourceMeta: {
1001
+ online: true,
1002
+ ...(successPayload.degraded ? { degraded: true } : {}),
1003
+ },
1004
+ };
1005
+ } catch (error) {
1006
+ return {
1007
+ payload: { sessions: [], processHints: [], degraded: true },
1008
+ sourceMeta: {
1009
+ online: false,
1010
+ degraded: true,
1011
+ reason: toErrorMessage(error),
1012
+ },
1013
+ };
1014
+ }
1015
+ }
1016
+
1017
+ async function getLocalSessionsResult(stickyBusyDelayMs: number): Promise<SessionsRouteResult> {
1018
+
1019
+ const { processes: rawProcessHints, timedOut: processDiscoveryTimedOut } =
1020
+ discoverOpencodeProcessCwdsWithoutPortWithMeta();
1021
+ const processHintsByDirectory = new Map<string, ProcessHint>();
1022
+ for (const process of rawProcessHints) {
1023
+ if (!process.cwd || process.cwd.startsWith('/private/tmp/opencode')) {
1024
+ continue;
1025
+ }
1026
+ if (processHintsByDirectory.has(process.cwd)) {
1027
+ continue;
1028
+ }
1029
+ processHintsByDirectory.set(process.cwd, {
1030
+ pid: process.pid,
1031
+ directory: process.cwd,
1032
+ projectName: getProjectName(process.cwd),
1033
+ reason: 'process_without_api_port',
1034
+ });
1035
+ }
1036
+
1037
+ const { ports, timedOut: portDiscoveryTimedOut } = discoverOpencodePortsWithMeta();
1038
+
1039
+ if (!ports.length) {
1040
+ const processHints = Array.from(processHintsByDirectory.values());
1041
+
1042
+ if (portDiscoveryTimedOut || processDiscoveryTimedOut) {
1043
+ return {
1044
+ payload: {
1045
+ error: 'OpenCode discovery timed out',
1046
+ hint: 'Host process discovery exceeded timeout. Retry shortly, or increase OPENCODE_DISCOVERY_TIMEOUT_MS.',
1047
+ ...(processHints.length > 0 ? { processHints } : {}),
1048
+ },
1049
+ status: 503,
1050
+ sourceMeta: {
1051
+ online: false,
1052
+ degraded: true,
1053
+ reason: 'OpenCode discovery timed out',
1054
+ },
1055
+ };
1056
+ }
1057
+
1058
+ if (processHints.length > 0) {
1059
+ return {
1060
+ payload: { sessions: [], processHints },
1061
+ sourceMeta: {
1062
+ online: false,
1063
+ reason: 'OpenCode server not found',
1064
+ },
1065
+ };
1066
+ }
1067
+
1068
+ return {
1069
+ payload: {
1070
+ error: 'OpenCode server not found',
1071
+ hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).'
1072
+ },
1073
+ status: 503,
1074
+ sourceMeta: {
1075
+ online: false,
1076
+ reason: 'OpenCode server not found',
1077
+ },
1078
+ };
1079
+ }
1080
+
1081
+ try {
1082
+ const results = await Promise.allSettled(ports.map(async (port) => {
1083
+ const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
1084
+ const sessionsResult = await withTimeout(
1085
+ (signal) => client.session.list({ signal }),
1086
+ sessionListTimeoutMs,
1087
+ `session.list(${port})`
1088
+ );
1089
+ const statusResult = await withTimeout(
1090
+ (signal) => client.session.status({ signal }),
1091
+ sessionStatusTimeoutMs,
1092
+ `session.status(${port})`
1093
+ ).catch(() => ({ data: {} }));
1094
+ return { port, client, sessions: sessionsResult.data || [], status: statusResult.data || {} };
1095
+ }));
1096
+
1097
+ const allSessions: SessionLike[] = [];
1098
+ const statusMap: Record<string, { type: 'idle' | 'busy' | 'retry' }> = {};
1099
+ const clientByPort: Record<number, ReturnType<typeof createOpencodeClient>> = {};
1100
+ const sessionPortMap: Record<string, number> = {};
1101
+ const failedPorts: Array<{ port: number; reason: string }> = [];
1102
+
1103
+ for (let i = 0; i < results.length; i++) {
1104
+ const r = results[i];
1105
+ const port = ports[i];
1106
+ if (r.status !== 'fulfilled') {
1107
+ failedPorts.push({
1108
+ port,
1109
+ reason: r.reason instanceof Error ? r.reason.message : String(r.reason),
1110
+ });
1111
+ continue;
1112
+ }
1113
+ allSessions.push(...r.value.sessions);
1114
+ Object.assign(statusMap, r.value.status);
1115
+ clientByPort[r.value.port] = r.value.client;
1116
+ for (const session of r.value.sessions as SessionLike[]) {
1117
+ if (!(session.id in sessionPortMap)) {
1118
+ sessionPortMap[session.id] = r.value.port;
1119
+ }
1120
+ }
1121
+ }
1122
+
1123
+ // Deduplicate by session.id
1124
+ const seen = new Set<string>();
1125
+ const sessions = allSessions.filter((session) => {
1126
+ if (seen.has(session.id)) return false;
1127
+ seen.add(session.id);
1128
+ return true;
1129
+ });
1130
+
1131
+ const parentSessions = sessions.filter((s) => !s.parentID);
1132
+ const childSessions = sessions.filter((s) => !!s.parentID);
1133
+
1134
+ const lifecycleNow = Date.now();
1135
+ pruneSessionForceUnarchived(lifecycleNow);
1136
+ pruneSessionStickyStatusBlocked(lifecycleNow);
1137
+
1138
+ for (const session of parentSessions) {
1139
+ if (session.time?.archived !== undefined && shouldForceSessionUnarchived(session.id, lifecycleNow)) {
1140
+ session.time = {
1141
+ ...session.time,
1142
+ archived: undefined,
1143
+ };
1144
+ }
1145
+ }
1146
+
1147
+ for (const child of childSessions) {
1148
+ if (child.time?.archived !== undefined && shouldForceSessionUnarchived(child.id, lifecycleNow)) {
1149
+ child.time = {
1150
+ ...child.time,
1151
+ archived: undefined,
1152
+ };
1153
+ }
1154
+ }
1155
+
1156
+ if (results.length > 0 && failedPorts.length === results.length) {
1157
+ pruneStickyState(Date.now(), new Set<string>());
1158
+ return {
1159
+ payload: {
1160
+ error: 'Failed to fetch sessions from OpenCode ports',
1161
+ hint: 'All discovered OpenCode API ports timed out or failed. Retry shortly or increase OPENCODE_SESSIONS_LIST_TIMEOUT_MS.',
1162
+ failedPorts,
1163
+ },
1164
+ status: 503,
1165
+ sourceMeta: {
1166
+ online: false,
1167
+ degraded: true,
1168
+ reason: 'Failed to fetch sessions from OpenCode ports',
1169
+ },
1170
+ };
1171
+ }
1172
+
1173
+ if (failedPorts.length > 0 && parentSessions.length === 0 && childSessions.length === 0) {
1174
+ pruneStickyState(Date.now(), new Set<string>());
1175
+ const processHints = Array.from(processHintsByDirectory.values());
1176
+ return {
1177
+ payload: {
1178
+ sessions: [],
1179
+ processHints,
1180
+ failedPorts,
1181
+ degraded: true,
1182
+ },
1183
+ sourceMeta: {
1184
+ online: true,
1185
+ degraded: true,
1186
+ },
1187
+ };
1188
+ }
1189
+
1190
+ // Enrich parent sessions
1191
+ const enrichedSessions: EnrichedSession[] = parentSessions.map((session) => {
1192
+ const projectName = getProjectName(session.directory);
1193
+ const branch = getGitBranch(session.directory);
1194
+ return {
1195
+ ...session,
1196
+ projectName,
1197
+ branch,
1198
+ realTimeStatus: statusMap[session.id]?.type || 'idle',
1199
+ waitingForUser: false,
1200
+ children: [],
1201
+ };
1202
+ });
1203
+
1204
+ const parentById = new Map(enrichedSessions.map((session) => [session.id, session]));
1205
+
1206
+ const now = Date.now();
1207
+ const unresolvedChildren: Array<{ parentId: string; child: SessionLike; childUpdatedAt: number }> = [];
1208
+
1209
+ // Enrich and nest child sessions under parents
1210
+ for (const child of childSessions) {
1211
+ // Find parent by parentID
1212
+ let parent = child.parentID
1213
+ ? enrichedSessions.find((session) => session.id === child.parentID)
1214
+ : null;
1215
+
1216
+ if (!parent) {
1217
+ const candidates = enrichedSessions
1218
+ .filter((session) => session.directory === child.directory)
1219
+ .sort((a, b) => getUpdatedAt(b) - getUpdatedAt(a));
1220
+
1221
+ parent =
1222
+ candidates.find((session) => session.realTimeStatus === 'busy' || session.realTimeStatus === 'retry') ||
1223
+ candidates[0];
1224
+ }
1225
+
1226
+ if (!parent) {
1227
+ continue;
1228
+ }
1229
+
1230
+ const statusFromMap = statusMap[child.id]?.type;
1231
+ const childUpdatedAt = getUpdatedAt(child);
1232
+ const isRecent = childUpdatedAt > 0 && now - childUpdatedAt <= CHILD_ACTIVE_WINDOW_MS;
1233
+ const shouldSkipArchivedChild = !!child.time?.archived && !statusFromMap && !isRecent;
1234
+
1235
+ if (shouldSkipArchivedChild) {
1236
+ continue;
1237
+ }
1238
+
1239
+ if (statusFromMap && statusFromMap !== 'idle') {
1240
+ parent.children.push(toChildEntry(child, statusFromMap));
1241
+ } else if (isRecent) {
1242
+ if (unresolvedChildren.length < CHILD_STATUS_MESSAGE_CHECK_LIMIT) {
1243
+ unresolvedChildren.push({ parentId: parent.id, child, childUpdatedAt });
1244
+ }
1245
+ } else {
1246
+ continue;
1247
+ }
1248
+ }
1249
+
1250
+ if (unresolvedChildren.length > 0) {
1251
+ const unresolvedChecks = await Promise.allSettled(
1252
+ unresolvedChildren.map(async ({ parentId, child, childUpdatedAt }) => {
1253
+ const port = sessionPortMap[child.id] ?? sessionPortMap[parentId];
1254
+ const client = port ? clientByPort[port] : undefined;
1255
+ const assumeBusyForUnknown =
1256
+ childUpdatedAt > 0 && now - childUpdatedAt <= CHILD_UNKNOWN_STATE_BUSY_WINDOW_MS;
1257
+ if (!client) {
1258
+ return {
1259
+ parentId,
1260
+ child,
1261
+ childStatus: assumeBusyForUnknown ? 'busy' as const : 'idle' as const,
1262
+ };
1263
+ }
1264
+
1265
+ try {
1266
+ const partStatuses = await fetchPartStatuses(client, child.id, sessionMessagesTimeoutMs);
1267
+ const hasRunningState = partStatuses.some((status) => status === 'running');
1268
+ const hasWaitingState = !hasRunningState && partStatuses.some(isWaitingPartStatus);
1269
+ const hasActiveState = hasWaitingState || hasRunningState;
1270
+ const recentlyActive = childUpdatedAt > 0 && now - childUpdatedAt <= 5 * 60 * 1000;
1271
+
1272
+ return {
1273
+ parentId,
1274
+ child,
1275
+ childWaitingForUser: hasWaitingState,
1276
+ childStatus: hasActiveState
1277
+ ? 'busy' as const
1278
+ : recentlyActive || assumeBusyForUnknown
1279
+ ? 'busy' as const
1280
+ : 'idle' as const,
1281
+ };
1282
+ } catch {
1283
+ return {
1284
+ parentId,
1285
+ child,
1286
+ childWaitingForUser: false,
1287
+ childStatus: assumeBusyForUnknown ? 'busy' as const : 'idle' as const,
1288
+ };
1289
+ }
1290
+ })
1291
+ );
1292
+
1293
+ for (const check of unresolvedChecks) {
1294
+ if (check.status !== 'fulfilled') continue;
1295
+ if (check.value.childStatus === 'idle') continue;
1296
+ const parent = parentById.get(check.value.parentId);
1297
+ if (!parent) continue;
1298
+ parent.children.push(toChildEntry(check.value.child, check.value.childStatus, check.value.childWaitingForUser));
1299
+ }
1300
+ }
1301
+
1302
+ const parentStatusFallbackCandidates = enrichedSessions
1303
+ .filter((session) => {
1304
+ if (session.realTimeStatus !== 'idle') return false;
1305
+ const updatedAt = getUpdatedAt(session);
1306
+ if (updatedAt > 0 && now - updatedAt <= CHILD_ACTIVE_WINDOW_MS) return true;
1307
+ return !!session.time?.archived;
1308
+ })
1309
+ .sort((a, b) => getUpdatedAt(b) - getUpdatedAt(a))
1310
+ .slice(0, CHILD_STATUS_MESSAGE_CHECK_LIMIT);
1311
+
1312
+ if (parentStatusFallbackCandidates.length > 0) {
1313
+ const parentFallbackChecks = await Promise.allSettled(
1314
+ parentStatusFallbackCandidates.map(async (session) => {
1315
+ const updatedAt = getUpdatedAt(session);
1316
+ const assumeBusyForUnknown =
1317
+ updatedAt > 0 && now - updatedAt <= CHILD_UNKNOWN_STATE_BUSY_WINDOW_MS;
1318
+ const port = sessionPortMap[session.id];
1319
+ const client = port ? clientByPort[port] : undefined;
1320
+
1321
+ if (!client) {
1322
+ return {
1323
+ sessionId: session.id,
1324
+ status: assumeBusyForUnknown ? 'busy' as const : 'idle' as const,
1325
+ waitingForUser: false,
1326
+ };
1327
+ }
1328
+
1329
+ try {
1330
+ const partStatuses = await fetchPartStatuses(client, session.id, sessionMessagesTimeoutMs);
1331
+ const hasRunningState = partStatuses.some((status) => status === 'running');
1332
+ const hasWaitingState = !hasRunningState && partStatuses.some(isWaitingPartStatus);
1333
+ const hasCompletedState =
1334
+ partStatuses.length > 0 && partStatuses.every((status) => status === 'completed');
1335
+ const recentlyActive = hasRecentActivity(session, now);
1336
+
1337
+ return {
1338
+ sessionId: session.id,
1339
+ status: hasRunningState || hasWaitingState
1340
+ ? 'busy' as const
1341
+ : hasCompletedState && !recentlyActive
1342
+ ? 'idle' as const
1343
+ : assumeBusyForUnknown || recentlyActive
1344
+ ? 'busy' as const
1345
+ : 'idle' as const,
1346
+ waitingForUser: hasWaitingState,
1347
+ };
1348
+ } catch {
1349
+ return {
1350
+ sessionId: session.id,
1351
+ status: assumeBusyForUnknown ? 'busy' as const : 'idle' as const,
1352
+ waitingForUser: false,
1353
+ };
1354
+ }
1355
+ })
1356
+ );
1357
+
1358
+ for (const check of parentFallbackChecks) {
1359
+ if (check.status !== 'fulfilled') continue;
1360
+ if (check.value.status === 'idle') continue;
1361
+ const session = parentById.get(check.value.sessionId);
1362
+ if (!session) continue;
1363
+ session.realTimeStatus = check.value.status;
1364
+ if (check.value.waitingForUser) {
1365
+ session.waitingForUser = true;
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ // Sort children for each parent: active first, then by updated time
1371
+ for (const session of enrichedSessions) {
1372
+ if (session.children.length > 0) {
1373
+ sortChildEntries(session.children);
1374
+ }
1375
+ }
1376
+
1377
+ const sessionsForInteractionChecks = enrichedSessions.filter(
1378
+ (session) =>
1379
+ session.realTimeStatus === 'busy' ||
1380
+ !!session.time?.archived ||
1381
+ session.children.some((child) => child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry')
1382
+ );
1383
+ if (sessionsForInteractionChecks.length > 0) {
1384
+ const pendingChecks = await Promise.allSettled(
1385
+ sessionsForInteractionChecks.map(async (session) => {
1386
+ const port = sessionPortMap[session.id];
1387
+ const client = port ? clientByPort[port] : undefined;
1388
+ if (!client) {
1389
+ return {
1390
+ sessionId: session.id,
1391
+ parentWaiting: false,
1392
+ waiting: false,
1393
+ running: false,
1394
+ waitingChildIds: new Set<string>(),
1395
+ };
1396
+ }
1397
+
1398
+ try {
1399
+ const partStatuses = await fetchPartStatuses(client, session.id, sessionMessagesTimeoutMs);
1400
+ const hasRunning = partStatuses.some((status) => status === 'running');
1401
+ const hasInteractionWait = !hasRunning && partStatuses.some(isWaitingPartStatus);
1402
+
1403
+ const childStateChecks = await Promise.allSettled(
1404
+ session.children
1405
+ .filter((child) => child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry')
1406
+ .map(async (child) => {
1407
+ const childPort = sessionPortMap[child.id] ?? sessionPortMap[session.id];
1408
+ const childClient = childPort ? clientByPort[childPort] : undefined;
1409
+ if (!childClient) {
1410
+ return { childId: child.id, waiting: false };
1411
+ }
1412
+ try {
1413
+ const childStatuses = await fetchPartStatuses(childClient, child.id, sessionMessagesTimeoutMs);
1414
+ const childHasRunning = childStatuses.some((status) => status === 'running');
1415
+ return {
1416
+ childId: child.id,
1417
+ waiting: !childHasRunning && childStatuses.some(isWaitingPartStatus),
1418
+ };
1419
+ } catch {
1420
+ return { childId: child.id, waiting: false };
1421
+ }
1422
+ })
1423
+ );
1424
+
1425
+ const waitingChildIds = new Set(
1426
+ childStateChecks
1427
+ .filter((result): result is PromiseFulfilledResult<{ childId: string; waiting: boolean }> => result.status === 'fulfilled')
1428
+ .filter((result) => result.value.waiting)
1429
+ .map((result) => result.value.childId)
1430
+ );
1431
+
1432
+ const hasWaitingChildren =
1433
+ waitingChildIds.size > 0 ||
1434
+ session.children.some((child) => child.waitingForUser || child.realTimeStatus === 'retry');
1435
+
1436
+ return {
1437
+ sessionId: session.id,
1438
+ parentWaiting: hasInteractionWait,
1439
+ waiting: hasInteractionWait || hasWaitingChildren,
1440
+ running: hasRunning,
1441
+ waitingChildIds,
1442
+ };
1443
+ } catch {
1444
+ return {
1445
+ sessionId: session.id,
1446
+ parentWaiting: false,
1447
+ waiting: false,
1448
+ running: false,
1449
+ waitingChildIds: new Set<string>(),
1450
+ };
1451
+ }
1452
+ })
1453
+ );
1454
+
1455
+ for (const result of pendingChecks) {
1456
+ if (result.status === 'fulfilled') {
1457
+ const session = enrichedSessions.find((candidate) => candidate.id === result.value.sessionId);
1458
+ if (!session) continue;
1459
+ for (const child of session.children) {
1460
+ if (result.value.waitingChildIds.has(child.id)) {
1461
+ child.waitingForUser = true;
1462
+ }
1463
+ }
1464
+ if (result.value.running) {
1465
+ session.realTimeStatus = 'busy';
1466
+ }
1467
+ if (result.value.parentWaiting) {
1468
+ session.waitingForUser = true;
1469
+ }
1470
+ }
1471
+ }
1472
+ }
1473
+
1474
+ const stickyNow = Date.now();
1475
+ const activeStickyIds = new Set<string>();
1476
+
1477
+ for (const session of enrichedSessions) {
1478
+ activeStickyIds.add(session.id);
1479
+ for (const child of session.children) {
1480
+ activeStickyIds.add(`child:${child.id}`);
1481
+ }
1482
+ }
1483
+
1484
+ for (const session of enrichedSessions) {
1485
+ if (shouldSkipSessionStatusStabilization(session, stickyNow)) {
1486
+ continue;
1487
+ }
1488
+
1489
+ applyStickyStatusStabilization(session, stickyNow, stickyBusyDelayMs);
1490
+ }
1491
+ pruneStickyState(stickyNow, activeStickyIds);
1492
+
1493
+ const knownDirectories = new Set<string>();
1494
+ for (const session of sessions) {
1495
+ if (session.directory) {
1496
+ knownDirectories.add(session.directory);
1497
+ }
1498
+ }
1499
+
1500
+ const processHints = Array.from(processHintsByDirectory.values()).filter(
1501
+ (hint) => !knownDirectories.has(hint.directory)
1502
+ );
1503
+
1504
+ const payload: SessionsSuccessPayload = {
1505
+ sessions: enrichedSessions,
1506
+ processHints,
1507
+ };
1508
+
1509
+ if (failedPorts.length > 0) {
1510
+ payload.failedPorts = failedPorts;
1511
+ payload.degraded = true;
1512
+ }
1513
+
1514
+ return {
1515
+ payload,
1516
+ sourceMeta: {
1517
+ online: true,
1518
+ ...(failedPorts.length > 0 ? { degraded: true } : {}),
1519
+ },
1520
+ };
1521
+ } catch (error) {
1522
+ console.error('Error fetching sessions:', error);
1523
+ return {
1524
+ payload: {
1525
+ error: 'Failed to fetch sessions',
1526
+ details: error instanceof Error ? error.message : String(error),
1527
+ hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).'
1528
+ },
1529
+ status: 500,
1530
+ sourceMeta: {
1531
+ online: false,
1532
+ degraded: true,
1533
+ reason: 'Failed to fetch sessions',
1534
+ },
1535
+ };
1536
+ }
1537
+ }
1538
+
1539
+ async function handleGet() {
1540
+ const stickyBusyDelayMs = await readStickyBusyDelayMs();
1541
+ return toRouteResponse(await getLocalSessionsResult(stickyBusyDelayMs));
1542
+ }
1543
+
1544
+ async function handlePost(request: Request) {
1545
+ let requestedSources: SessionSource[];
1546
+
1547
+ try {
1548
+ const body = await request.json();
1549
+ requestedSources = parseRequestedSources(body);
1550
+ } catch {
1551
+ return Response.json(
1552
+ {
1553
+ error: 'Invalid sources payload',
1554
+ hint: 'POST /api/sessions expects a JSON body with a non-empty sources array.',
1555
+ },
1556
+ { status: 400 }
1557
+ );
1558
+ }
1559
+
1560
+ const isNodeRuntime = process.env[RUNTIME_ROLE_ENV_VAR] === 'node';
1561
+ const enabledSources = isNodeRuntime
1562
+ ? [LOCAL_SOURCE]
1563
+ : requestedSources.filter((source) => source.hostKind === 'local' || source.enabled);
1564
+
1565
+ if (enabledSources.length === 0) {
1566
+ return Response.json({ sessions: [], processHints: [], hosts: [], hostStatuses: [] });
1567
+ }
1568
+
1569
+ if (enabledSources.length === 1 && enabledSources[0].hostKind === 'local') {
1570
+ const stickyBusyDelayMs = await readStickyBusyDelayMs();
1571
+ const localResult = await getLocalSessionsResult(stickyBusyDelayMs);
1572
+ const rawLocalMeta = localResult.sourceMeta ?? {
1573
+ online: !localResult.status,
1574
+ ...(localResult.status ? { degraded: true } : {}),
1575
+ };
1576
+ const localMeta = rawLocalMeta.online
1577
+ ? rawLocalMeta
1578
+ : {
1579
+ ...rawLocalMeta,
1580
+ degraded: true,
1581
+ };
1582
+ const localStatus = toHostStatus(LOCAL_SOURCE, localMeta);
1583
+ const normalizedOfflinePayload =
1584
+ !localMeta.online && isRecord(localResult.payload)
1585
+ ? withHostAliases(
1586
+ {
1587
+ sessions: [],
1588
+ processHints: Array.isArray(localResult.payload['processHints'])
1589
+ ? (localResult.payload['processHints'] as ProcessHint[])
1590
+ : [],
1591
+ degraded: true,
1592
+ },
1593
+ [localStatus]
1594
+ )
1595
+ : null;
1596
+
1597
+ if (normalizedOfflinePayload) {
1598
+ const statusCode = localResult.status ?? 503;
1599
+ return Response.json(normalizedOfflinePayload, { status: statusCode });
1600
+ }
1601
+
1602
+ return toRouteResponse({
1603
+ ...localResult,
1604
+ payload: isRecord(localResult.payload)
1605
+ ? withHostAliases(addHostMetadataToPayload(localResult.payload, LOCAL_SOURCE), [localStatus])
1606
+ : localResult.payload,
1607
+ });
1608
+ }
1609
+
1610
+ const stickyBusyDelayMs = enabledSources.some((source) => source.hostKind === 'local')
1611
+ ? await readStickyBusyDelayMs()
1612
+ : 0;
1613
+
1614
+ const nodeRecords = await listNodeRecords();
1615
+ const nodeRecordsById = new Map(nodeRecords.map((record) => [record.nodeId, record]));
1616
+ const resolvedSources = enabledSources.map((source) => {
1617
+ if (!isRemoteSource(source)) {
1618
+ return source;
1619
+ }
1620
+
1621
+ const nodeRecord = nodeRecordsById.get(source.hostId);
1622
+ if (!nodeRecord) {
1623
+ return source;
1624
+ }
1625
+
1626
+ return {
1627
+ ...source,
1628
+ baseUrl: nodeRecord.baseUrl,
1629
+ enabled: nodeRecord.enabled,
1630
+ };
1631
+ });
1632
+
1633
+ const sourceResults = await Promise.allSettled(
1634
+ resolvedSources.map(async (source) => ({
1635
+ source,
1636
+ result: source.hostKind === 'local'
1637
+ ? await getLocalSessionsResult(stickyBusyDelayMs)
1638
+ : await getRemoteNodeSessionsResult(source, nodeRecordsById.get(source.hostId)),
1639
+ }))
1640
+ );
1641
+
1642
+ const hostStatuses: SessionHostStatus[] = [];
1643
+ const aggregateSessions: EnrichedSession[] = [];
1644
+ const aggregateProcessHints: ProcessHint[] = [];
1645
+ let degraded = false;
1646
+
1647
+ for (const sourceResult of sourceResults) {
1648
+ if (sourceResult.status !== 'fulfilled') {
1649
+ degraded = true;
1650
+ continue;
1651
+ }
1652
+
1653
+ const { source, result } = sourceResult.value;
1654
+ const meta = result.sourceMeta ?? {
1655
+ online: !result.status,
1656
+ ...(result.status ? { degraded: true } : {}),
1657
+ };
1658
+ const payload = readSuccessPayload(result);
1659
+
1660
+ let sourceMetadataIssue = false;
1661
+ aggregateProcessHints.push(...payload.processHints);
1662
+
1663
+ for (const session of payload.sessions) {
1664
+ const enrichedSession = addHostMetadataToSession(session, source);
1665
+ if (enrichedSession) {
1666
+ aggregateSessions.push(enrichedSession);
1667
+ continue;
1668
+ }
1669
+
1670
+ sourceMetadataIssue = true;
1671
+ }
1672
+
1673
+ const hostMeta = sourceMetadataIssue
1674
+ ? {
1675
+ ...meta,
1676
+ degraded: true,
1677
+ reason: meta.reason ?? 'node_payload_invalid_session_id',
1678
+ }
1679
+ : meta;
1680
+
1681
+ hostStatuses.push(toHostStatus(source, hostMeta));
1682
+
1683
+ if (!hostMeta.online || hostMeta.degraded || payload.degraded) {
1684
+ degraded = true;
1685
+ }
1686
+ }
1687
+
1688
+ return Response.json({
1689
+ sessions: aggregateSessions,
1690
+ processHints: aggregateProcessHints,
1691
+ hosts: hostStatuses,
1692
+ hostStatuses,
1693
+ ...(degraded ? { degraded: true } : {}),
1694
+ });
1695
+ }