@aoagents/ao-web 0.6.0 → 0.7.0

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 (268) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +200 -186
  3. package/.next/app-path-routes-manifest.json +16 -14
  4. package/.next/build-manifest.json +6 -6
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/prerender-manifest.json +19 -19
  7. package/.next/react-loadable-manifest.json +5 -3
  8. package/.next/required-server-files.json +4 -5
  9. package/.next/server/app/_not-found/page.js +2 -2
  10. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +8 -8
  14. package/.next/server/app/api/backlog/route.js +1 -1
  15. package/.next/server/app/api/backlog/route.js.nft.json +1 -1
  16. package/.next/server/app/api/backlog/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/browse-directory/route.js +1 -1
  18. package/.next/server/app/api/browse-directory/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/filesystem/browse/route.js +1 -1
  20. package/.next/server/app/api/filesystem/browse/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/issues/route.js +1 -1
  22. package/.next/server/app/api/issues/route.js.nft.json +1 -1
  23. package/.next/server/app/api/issues/route_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/observability/route.js +1 -1
  25. package/.next/server/app/api/observability/route.js.nft.json +1 -1
  26. package/.next/server/app/api/observability/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/orchestrators/route.js +1 -1
  28. package/.next/server/app/api/orchestrators/route.js.nft.json +1 -1
  29. package/.next/server/app/api/orchestrators/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/projects/[id]/route.js +5 -1
  31. package/.next/server/app/api/projects/[id]/route.js.nft.json +1 -1
  32. package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/projects/reload/route.js +1 -1
  34. package/.next/server/app/api/projects/reload/route.js.nft.json +1 -1
  35. package/.next/server/app/api/projects/reload/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/projects/route.js +1 -1
  37. package/.next/server/app/api/projects/route.js.nft.json +1 -1
  38. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/prs/[id]/merge/route.js +1 -1
  40. package/.next/server/app/api/prs/[id]/merge/route.js.nft.json +1 -1
  41. package/.next/server/app/api/prs/[id]/merge/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/runtime/terminal/route.js +1 -1
  43. package/.next/server/app/api/runtime/terminal/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/sessions/[id]/kill/route.js +1 -1
  45. package/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
  46. package/.next/server/app/api/sessions/[id]/kill/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/sessions/[id]/message/route.js +1 -1
  48. package/.next/server/app/api/sessions/[id]/message/route.js.nft.json +1 -1
  49. package/.next/server/app/api/sessions/[id]/message/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/sessions/[id]/remap/route.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/remap/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/[id]/remap/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/sessions/[id]/restore/route.js +1 -1
  54. package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
  55. package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  57. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  58. package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/sessions/[id]/send/route.js +1 -1
  60. package/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -1
  61. package/.next/server/app/api/sessions/[id]/send/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/sessions/patches/route.js +1 -1
  63. package/.next/server/app/api/sessions/patches/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sessions/patches/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/sessions/route.js +1 -1
  66. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  67. package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/setup-labels/route.js +1 -1
  69. package/.next/server/app/api/setup-labels/route.js.nft.json +1 -1
  70. package/.next/server/app/api/setup-labels/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/spawn/route.js +1 -1
  72. package/.next/server/app/api/spawn/route.js.nft.json +1 -1
  73. package/.next/server/app/api/spawn/route_client-reference-manifest.js +1 -1
  74. package/.next/server/app/api/update/route.js +1 -0
  75. package/.next/server/app/api/update/route.js.nft.json +1 -0
  76. package/.next/server/app/api/update/route_client-reference-manifest.js +1 -0
  77. package/.next/server/app/api/verify/route.js +1 -1
  78. package/.next/server/app/api/verify/route.js.nft.json +1 -1
  79. package/.next/server/app/api/verify/route_client-reference-manifest.js +1 -1
  80. package/.next/server/app/api/version/route.js +1 -0
  81. package/.next/server/app/api/version/route.js.nft.json +1 -0
  82. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -0
  83. package/.next/server/app/api/webhooks/[...slug]/route.js +1 -1
  84. package/.next/server/app/api/webhooks/[...slug]/route.js.nft.json +1 -1
  85. package/.next/server/app/api/webhooks/[...slug]/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/apple-icon/route.js +1 -1
  87. package/.next/server/app/apple-icon/route.js.nft.json +1 -1
  88. package/.next/server/app/apple-icon/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/apple-icon.body +0 -0
  90. package/.next/server/app/dev/terminal-test/page.js +3 -3
  91. package/.next/server/app/dev/terminal-test/page.js.nft.json +1 -1
  92. package/.next/server/app/dev/terminal-test/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app/dev/terminal-test.html +1 -1
  94. package/.next/server/app/dev/terminal-test.rsc +9 -9
  95. package/.next/server/app/icon/route.js +1 -1
  96. package/.next/server/app/icon/route.js.nft.json +1 -1
  97. package/.next/server/app/icon/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/icon-192/route.js +1 -1
  99. package/.next/server/app/icon-192/route.js.nft.json +1 -1
  100. package/.next/server/app/icon-192/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/icon-512/route.js +1 -1
  102. package/.next/server/app/icon-512/route.js.nft.json +1 -1
  103. package/.next/server/app/icon-512/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/icon.body +0 -0
  105. package/.next/server/app/manifest.webmanifest/route.js +1 -1
  106. package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
  107. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  108. package/.next/server/app/manifest.webmanifest.body +1 -1
  109. package/.next/server/app/orchestrators/page.js +2 -2
  110. package/.next/server/app/orchestrators/page.js.nft.json +1 -1
  111. package/.next/server/app/orchestrators/page_client-reference-manifest.js +1 -1
  112. package/.next/server/app/page.js +2 -2
  113. package/.next/server/app/page.js.nft.json +1 -1
  114. package/.next/server/app/page_client-reference-manifest.js +1 -1
  115. package/.next/server/app/projects/[projectId]/page.js +2 -2
  116. package/.next/server/app/projects/[projectId]/page.js.nft.json +1 -1
  117. package/.next/server/app/projects/[projectId]/page_client-reference-manifest.js +1 -1
  118. package/.next/server/app/projects/[projectId]/sessions/[id]/page.js +2 -2
  119. package/.next/server/app/projects/[projectId]/sessions/[id]/page.js.nft.json +1 -1
  120. package/.next/server/app/projects/[projectId]/sessions/[id]/page_client-reference-manifest.js +1 -1
  121. package/.next/server/app/projects/[projectId]/settings/page.js +2 -2
  122. package/.next/server/app/projects/[projectId]/settings/page.js.nft.json +1 -1
  123. package/.next/server/app/projects/[projectId]/settings/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/prs/page.js +2 -2
  125. package/.next/server/app/prs/page.js.nft.json +1 -1
  126. package/.next/server/app/prs/page_client-reference-manifest.js +1 -1
  127. package/.next/server/app/sessions/[id]/page.js +2 -2
  128. package/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  129. package/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  130. package/.next/server/app/test-direct/page.js +2 -2
  131. package/.next/server/app/test-direct/page.js.nft.json +1 -1
  132. package/.next/server/app/test-direct/page_client-reference-manifest.js +1 -1
  133. package/.next/server/app/test-direct.html +1 -1
  134. package/.next/server/app/test-direct.rsc +9 -9
  135. package/.next/server/app-paths-manifest.json +16 -14
  136. package/.next/server/chunks/1271.js +1 -1
  137. package/.next/server/chunks/1876.js +2 -2
  138. package/.next/server/chunks/303.js +3 -0
  139. package/.next/server/chunks/3714.js +1 -1
  140. package/.next/server/chunks/4520.js +1 -1
  141. package/.next/server/chunks/6013.js +884 -0
  142. package/.next/server/chunks/6848.js +1 -0
  143. package/.next/server/chunks/7167.js +1 -1
  144. package/.next/server/chunks/7173.js +1 -1
  145. package/.next/server/chunks/7227.js +604 -0
  146. package/.next/server/middleware-build-manifest.js +1 -1
  147. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  148. package/.next/server/pages/404.html +1 -1
  149. package/.next/server/pages/500.html +1 -1
  150. package/.next/server/server-reference-manifest.json +1 -1
  151. package/.next/static/9nr0fNWbZcuWTqhM2HhrH/_buildManifest.js +1 -0
  152. package/.next/static/chunks/1654.ac304fc9e36ec94a.js +1 -0
  153. package/.next/static/chunks/3764.cdef4e76dbc23af8.js +1 -0
  154. package/.next/static/chunks/3780-7bdc52d8370adf2f.js +1 -0
  155. package/.next/static/chunks/{5795-b96fd46c8c7344fc.js → 5795-a4dd81606df09bc4.js} +1 -1
  156. package/.next/static/chunks/6231-57dd9c1e306c7069.js +1 -0
  157. package/.next/static/chunks/app/_not-found/page-8b5044bdc951ae98.js +1 -0
  158. package/.next/static/chunks/app/api/backlog/route-8b5044bdc951ae98.js +1 -0
  159. package/.next/static/chunks/app/api/browse-directory/route-8b5044bdc951ae98.js +1 -0
  160. package/.next/static/chunks/app/api/filesystem/browse/route-8b5044bdc951ae98.js +1 -0
  161. package/.next/static/chunks/app/api/issues/route-8b5044bdc951ae98.js +1 -0
  162. package/.next/static/chunks/app/api/observability/route-8b5044bdc951ae98.js +1 -0
  163. package/.next/static/chunks/app/api/orchestrators/route-8b5044bdc951ae98.js +1 -0
  164. package/.next/static/chunks/app/api/projects/[id]/route-8b5044bdc951ae98.js +1 -0
  165. package/.next/static/chunks/app/api/projects/reload/route-8b5044bdc951ae98.js +1 -0
  166. package/.next/static/chunks/app/api/projects/route-8b5044bdc951ae98.js +1 -0
  167. package/.next/static/chunks/app/api/prs/[id]/merge/route-8b5044bdc951ae98.js +1 -0
  168. package/.next/static/chunks/app/api/runtime/terminal/route-8b5044bdc951ae98.js +1 -0
  169. package/.next/static/chunks/app/api/sessions/[id]/kill/route-8b5044bdc951ae98.js +1 -0
  170. package/.next/static/chunks/app/api/sessions/[id]/message/route-8b5044bdc951ae98.js +1 -0
  171. package/.next/static/chunks/app/api/sessions/[id]/remap/route-8b5044bdc951ae98.js +1 -0
  172. package/.next/static/chunks/app/api/sessions/[id]/restore/route-8b5044bdc951ae98.js +1 -0
  173. package/.next/static/chunks/app/api/sessions/[id]/route-8b5044bdc951ae98.js +1 -0
  174. package/.next/static/chunks/app/api/sessions/[id]/send/route-8b5044bdc951ae98.js +1 -0
  175. package/.next/static/chunks/app/api/sessions/patches/route-8b5044bdc951ae98.js +1 -0
  176. package/.next/static/chunks/app/api/sessions/route-8b5044bdc951ae98.js +1 -0
  177. package/.next/static/chunks/app/api/setup-labels/route-8b5044bdc951ae98.js +1 -0
  178. package/.next/static/chunks/app/api/spawn/route-8b5044bdc951ae98.js +1 -0
  179. package/.next/static/chunks/app/api/update/route-8b5044bdc951ae98.js +1 -0
  180. package/.next/static/chunks/app/api/verify/route-8b5044bdc951ae98.js +1 -0
  181. package/.next/static/chunks/app/api/version/route-8b5044bdc951ae98.js +1 -0
  182. package/.next/static/chunks/app/api/webhooks/[...slug]/route-8b5044bdc951ae98.js +1 -0
  183. package/.next/static/chunks/app/apple-icon/route-8b5044bdc951ae98.js +1 -0
  184. package/.next/static/chunks/app/dev/terminal-test/{page-59decd1aad4e02ad.js → page-9aa423dfd54c8325.js} +1 -1
  185. package/.next/static/chunks/app/{error-684a1c5596fa1e14.js → error-65c526052680c0dc.js} +1 -1
  186. package/.next/static/chunks/app/{global-error-1a79bacfbd9b1ba4.js → global-error-63dcb797b2c3ee60.js} +1 -1
  187. package/.next/static/chunks/app/icon/route-8b5044bdc951ae98.js +1 -0
  188. package/.next/static/chunks/app/icon-192/route-8b5044bdc951ae98.js +1 -0
  189. package/.next/static/chunks/app/icon-512/route-8b5044bdc951ae98.js +1 -0
  190. package/.next/static/chunks/app/layout-36ab0168ddb22083.js +1 -0
  191. package/.next/static/chunks/app/loading-8b5044bdc951ae98.js +1 -0
  192. package/.next/static/chunks/app/manifest.webmanifest/route-8b5044bdc951ae98.js +1 -0
  193. package/.next/static/chunks/app/not-found-a693bed1f9e1893f.js +1 -0
  194. package/.next/static/chunks/app/orchestrators/{page-e3a2c53b57dd8391.js → page-376a92db51deb112.js} +1 -1
  195. package/.next/static/chunks/app/page-587d546e62c0796f.js +1 -0
  196. package/.next/static/chunks/app/projects/[projectId]/loading-8b5044bdc951ae98.js +1 -0
  197. package/.next/static/chunks/app/projects/[projectId]/page-bd8fc2a1decb649d.js +1 -0
  198. package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-bd33f6ffda513080.js +1 -0
  199. package/.next/static/chunks/app/projects/[projectId]/settings/page-11facc471a63de50.js +1 -0
  200. package/.next/static/chunks/app/prs/page-f34f66ad51106080.js +1 -0
  201. package/.next/static/chunks/app/sessions/[id]/{error-62e9972b39d9cd16.js → error-df65e7b626bbb713.js} +1 -1
  202. package/.next/static/chunks/app/sessions/[id]/loading-8b5044bdc951ae98.js +1 -0
  203. package/.next/static/chunks/app/sessions/[id]/not-found-a693bed1f9e1893f.js +1 -0
  204. package/.next/static/chunks/app/sessions/[id]/page-3ea4aa79275ea449.js +1 -0
  205. package/.next/static/chunks/app/test-direct/page-edfc701a9300105b.js +1 -0
  206. package/.next/static/chunks/main-app-decbc53736801215.js +1 -0
  207. package/.next/static/chunks/{webpack-83d2d8248a30259c.js → webpack-ecf0988dbb79e19b.js} +1 -1
  208. package/.next/static/css/b93232cd4a58743d.css +1 -0
  209. package/dist-server/direct-terminal-ws.js +12 -2
  210. package/dist-server/mux-websocket.js +270 -70
  211. package/dist-server/start-all.js +28 -6
  212. package/dist-server/tmux-utils.js +124 -2
  213. package/next.config.js +26 -0
  214. package/package.json +26 -12
  215. package/.next/server/chunks/1172.js +0 -1
  216. package/.next/server/chunks/6811.js +0 -3
  217. package/.next/server/chunks/801.js +0 -658
  218. package/.next/server/chunks/9223.js +0 -440
  219. package/.next/static/chunks/1383.8f5f7d4606d356cc.js +0 -1
  220. package/.next/static/chunks/3764.88619fb0d047cae8.js +0 -1
  221. package/.next/static/chunks/3780-52c4733ce6591b8d.js +0 -1
  222. package/.next/static/chunks/4465-17154f7a01abfe85.js +0 -1
  223. package/.next/static/chunks/app/_not-found/page-3b8a01e726e988c8.js +0 -1
  224. package/.next/static/chunks/app/api/backlog/route-3b8a01e726e988c8.js +0 -1
  225. package/.next/static/chunks/app/api/browse-directory/route-3b8a01e726e988c8.js +0 -1
  226. package/.next/static/chunks/app/api/filesystem/browse/route-3b8a01e726e988c8.js +0 -1
  227. package/.next/static/chunks/app/api/issues/route-3b8a01e726e988c8.js +0 -1
  228. package/.next/static/chunks/app/api/observability/route-3b8a01e726e988c8.js +0 -1
  229. package/.next/static/chunks/app/api/orchestrators/route-3b8a01e726e988c8.js +0 -1
  230. package/.next/static/chunks/app/api/projects/[id]/route-3b8a01e726e988c8.js +0 -1
  231. package/.next/static/chunks/app/api/projects/reload/route-3b8a01e726e988c8.js +0 -1
  232. package/.next/static/chunks/app/api/projects/route-3b8a01e726e988c8.js +0 -1
  233. package/.next/static/chunks/app/api/prs/[id]/merge/route-3b8a01e726e988c8.js +0 -1
  234. package/.next/static/chunks/app/api/runtime/terminal/route-3b8a01e726e988c8.js +0 -1
  235. package/.next/static/chunks/app/api/sessions/[id]/kill/route-3b8a01e726e988c8.js +0 -1
  236. package/.next/static/chunks/app/api/sessions/[id]/message/route-3b8a01e726e988c8.js +0 -1
  237. package/.next/static/chunks/app/api/sessions/[id]/remap/route-3b8a01e726e988c8.js +0 -1
  238. package/.next/static/chunks/app/api/sessions/[id]/restore/route-3b8a01e726e988c8.js +0 -1
  239. package/.next/static/chunks/app/api/sessions/[id]/route-3b8a01e726e988c8.js +0 -1
  240. package/.next/static/chunks/app/api/sessions/[id]/send/route-3b8a01e726e988c8.js +0 -1
  241. package/.next/static/chunks/app/api/sessions/patches/route-3b8a01e726e988c8.js +0 -1
  242. package/.next/static/chunks/app/api/sessions/route-3b8a01e726e988c8.js +0 -1
  243. package/.next/static/chunks/app/api/setup-labels/route-3b8a01e726e988c8.js +0 -1
  244. package/.next/static/chunks/app/api/spawn/route-3b8a01e726e988c8.js +0 -1
  245. package/.next/static/chunks/app/api/verify/route-3b8a01e726e988c8.js +0 -1
  246. package/.next/static/chunks/app/api/webhooks/[...slug]/route-3b8a01e726e988c8.js +0 -1
  247. package/.next/static/chunks/app/apple-icon/route-3b8a01e726e988c8.js +0 -1
  248. package/.next/static/chunks/app/icon/route-3b8a01e726e988c8.js +0 -1
  249. package/.next/static/chunks/app/icon-192/route-3b8a01e726e988c8.js +0 -1
  250. package/.next/static/chunks/app/icon-512/route-3b8a01e726e988c8.js +0 -1
  251. package/.next/static/chunks/app/layout-0cda720d75f111b8.js +0 -1
  252. package/.next/static/chunks/app/loading-3b8a01e726e988c8.js +0 -1
  253. package/.next/static/chunks/app/manifest.webmanifest/route-3b8a01e726e988c8.js +0 -1
  254. package/.next/static/chunks/app/not-found-315f4e5c106b425d.js +0 -1
  255. package/.next/static/chunks/app/page-e2e589ea11a0780a.js +0 -1
  256. package/.next/static/chunks/app/projects/[projectId]/loading-3b8a01e726e988c8.js +0 -1
  257. package/.next/static/chunks/app/projects/[projectId]/page-19a6d4cc9951a9e4.js +0 -1
  258. package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-75c536c9755754f7.js +0 -1
  259. package/.next/static/chunks/app/projects/[projectId]/settings/page-f8c323b91978efff.js +0 -1
  260. package/.next/static/chunks/app/prs/page-08f17d7dc341b6f1.js +0 -1
  261. package/.next/static/chunks/app/sessions/[id]/loading-3b8a01e726e988c8.js +0 -1
  262. package/.next/static/chunks/app/sessions/[id]/not-found-315f4e5c106b425d.js +0 -1
  263. package/.next/static/chunks/app/sessions/[id]/page-d0b08722dec5a04a.js +0 -1
  264. package/.next/static/chunks/app/test-direct/page-ee0944bcd355194e.js +0 -1
  265. package/.next/static/chunks/main-app-b95f197c38e3b0a3.js +0 -1
  266. package/.next/static/css/cf9226160e230bf4.css +0 -1
  267. package/.next/static/h0RXs0prE87a8wlbEXQKM/_buildManifest.js +0 -1
  268. /package/.next/static/{h0RXs0prE87a8wlbEXQKM → 9nr0fNWbZcuWTqhM2HhrH}/_ssgManifest.js +0 -0
@@ -6,9 +6,10 @@
6
6
  * every 3s, then broadcast to all subscribed clients via WebSocket.
7
7
  */
8
8
  import { WebSocketServer, WebSocket } from "ws";
9
- import { homedir, userInfo } from "node:os";
10
9
  import { spawn } from "node:child_process";
11
- import { findTmux, resolveTmuxSession, validateSessionId } from "./tmux-utils.js";
10
+ import { connect as netConnect } from "node:net";
11
+ import { findTmux, resolveTmuxSession, resolvePipePath, tmuxHasSession, validateSessionId, } from "./tmux-utils.js";
12
+ import { getEnvDefaults, isWindows } from "@aoagents/ao-core";
12
13
  /**
13
14
  * Manages polling of session patches from Next.js /api/sessions/patches.
14
15
  * Broadcasts to all subscribed callbacks.
@@ -161,7 +162,11 @@ export class TerminalManager {
161
162
  terminals = new Map();
162
163
  TMUX;
163
164
  constructor(tmuxPath) {
164
- this.TMUX = tmuxPath ?? findTmux();
165
+ const resolved = tmuxPath ?? findTmux();
166
+ if (!resolved) {
167
+ throw new Error("tmux not available on this platform");
168
+ }
169
+ this.TMUX = resolved;
165
170
  }
166
171
  terminalKey(id, projectId) {
167
172
  return projectId ? `${projectId}:${id}` : id;
@@ -215,16 +220,16 @@ export class TerminalManager {
215
220
  console.error(`[MuxServer] Failed to hide status bar for ${tmuxSessionId}:`, err.message);
216
221
  });
217
222
  // Build environment
218
- const homeDir = process.env.HOME || homedir();
219
- const currentUser = process.env.USER || userInfo().username;
223
+ const platformDefaults = getEnvDefaults();
224
+ const homeDir = platformDefaults.HOME;
220
225
  const env = {
221
- HOME: homeDir,
222
- SHELL: process.env.SHELL || "/bin/bash",
223
- USER: currentUser,
224
- PATH: process.env.PATH || "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin",
226
+ HOME: platformDefaults.HOME,
227
+ SHELL: platformDefaults.SHELL,
228
+ USER: platformDefaults.USER,
229
+ PATH: process.env.PATH || platformDefaults.PATH,
225
230
  TERM: "xterm-256color",
226
231
  LANG: process.env.LANG || "en_US.UTF-8",
227
- TMPDIR: process.env.TMPDIR || "/tmp",
232
+ TMPDIR: platformDefaults.TMPDIR,
228
233
  };
229
234
  if (!ptySpawn) {
230
235
  throw new Error("node-pty not available");
@@ -280,9 +285,35 @@ export class TerminalManager {
280
285
  }
281
286
  });
282
287
  // Handle PTY exit
283
- pty.onExit(({ exitCode }) => {
288
+ //
289
+ // Async: the has-session probe shells out via promisified execFile and
290
+ // must be awaited. node-pty fires onExit on the main thread; a sync
291
+ // probe would freeze the entire web server (every WebSocket, HTTP
292
+ // request, in-flight terminal) for up to the subprocess timeout when
293
+ // tmux is slow to respond.
294
+ pty.onExit(async ({ exitCode }) => {
284
295
  console.log(`[MuxServer] PTY exited for ${id} with code ${exitCode}`);
285
296
  terminal.pty = null;
297
+ // Skip the re-attach loop entirely when the underlying tmux session is
298
+ // gone (e.g. user pressed Ctrl-C in the pane and the launch command
299
+ // exited, taking the only window with it). Without this guard we
300
+ // burn three doomed attach-session spawns and emit a noisy
301
+ // "Max re-attach attempts reached" log line for what is actually a
302
+ // clean user-initiated termination — see issue #1756. The
303
+ // MAX_REATTACH_ATTEMPTS bound from #1640 still covers tmux server
304
+ // hiccups where the session does still exist.
305
+ if (terminal.subscribers.size > 0 &&
306
+ !(await tmuxHasSession(this.TMUX, tmuxSessionId))) {
307
+ console.log(`[MuxServer] tmux session ${tmuxSessionId} is gone, not re-attaching`);
308
+ if (terminal.resetTimer) {
309
+ clearTimeout(terminal.resetTimer);
310
+ terminal.resetTimer = undefined;
311
+ }
312
+ for (const cb of terminal.exitCallbacks) {
313
+ cb(exitCode);
314
+ }
315
+ return;
316
+ }
286
317
  // Re-attach if subscribers are still present, up to MAX_REATTACH_ATTEMPTS.
287
318
  // The cap prevents an unbounded respawn loop when the PTY crashes immediately
288
319
  // after every attach (e.g. resource exhaustion or a broken tmux session).
@@ -378,22 +409,163 @@ export class TerminalManager {
378
409
  return terminal.buffer.join("");
379
410
  }
380
411
  }
412
+ /**
413
+ * Handle a Windows terminal message by relaying through named pipes.
414
+ * Extracted from the WebSocket connection handler for testability.
415
+ */
416
+ export function handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, deps) {
417
+ const WS_OPEN = 1; // WebSocket.OPEN
418
+ const { id, type, projectId } = msg;
419
+ // MuxProvider keys subscribers under `${projectId}:${id}` when projectId is
420
+ // provided, so every outbound terminal message must echo projectId back —
421
+ // otherwise the client routes by id alone and the subscriber bucket
422
+ // mismatches, leaving the xterm pane blank on /projects/[id]/sessions/[id].
423
+ const echo = projectId ? { projectId } : {};
424
+ // Project-scoped pipe-map key: matches the Unix `subscriptionKey` shape so
425
+ // two projects sharing a sessionId on the same mux connection don't collide
426
+ // on the same socket/buffer entry.
427
+ const pipeKey = projectId ? `${projectId}:${id}` : id;
428
+ // The Unix path validates inside TerminalManager.open(). The Windows pipe
429
+ // relay bypasses TerminalManager entirely, so validate here too — `id`
430
+ // becomes a map key and is constructed into a pipe path downstream.
431
+ if (!validateSessionId(id)) {
432
+ if (ws.readyState === WS_OPEN) {
433
+ ws.send(JSON.stringify({
434
+ ch: "terminal",
435
+ id,
436
+ type: "error",
437
+ message: "invalid session id",
438
+ ...echo,
439
+ }));
440
+ }
441
+ return;
442
+ }
443
+ if (type === "open") {
444
+ if (winPipes.has(pipeKey)) {
445
+ ws.send(JSON.stringify({ ch: "terminal", id, type: "opened", ...echo }));
446
+ }
447
+ else {
448
+ const pipePath = deps.resolvePipePath(id, projectId);
449
+ if (!pipePath) {
450
+ throw new Error(`No PTY host pipe found for session ${id}`);
451
+ }
452
+ const pipeSocket = deps.connect(pipePath);
453
+ winPipes.set(pipeKey, pipeSocket);
454
+ winPipeBuffers.set(pipeKey, Buffer.alloc(0));
455
+ pipeSocket.on("error", (err) => {
456
+ winPipes.delete(pipeKey);
457
+ winPipeBuffers.delete(pipeKey);
458
+ pipeSocket.destroy();
459
+ if (ws.readyState === WS_OPEN) {
460
+ ws.send(JSON.stringify({
461
+ ch: "terminal",
462
+ id,
463
+ type: "error",
464
+ message: `PTY host not available: ${err.message}`,
465
+ ...echo,
466
+ }));
467
+ }
468
+ });
469
+ pipeSocket.on("connect", () => {
470
+ if (ws.readyState === WS_OPEN) {
471
+ ws.send(JSON.stringify({ ch: "terminal", id, type: "opened", ...echo }));
472
+ }
473
+ pipeSocket.on("data", (chunk) => {
474
+ const existing = winPipeBuffers.get(pipeKey) ?? Buffer.alloc(0);
475
+ let buf = Buffer.concat([existing, chunk]);
476
+ winPipeBuffers.set(pipeKey, buf);
477
+ while (buf.length >= 5) {
478
+ const msgType = buf.readUInt8(0);
479
+ const length = buf.readUInt32BE(1);
480
+ if (buf.length < 5 + length)
481
+ break;
482
+ const payload = buf.subarray(5, 5 + length);
483
+ buf = buf.subarray(5 + length);
484
+ winPipeBuffers.set(pipeKey, buf);
485
+ if (msgType === 0x01 && ws.readyState === WS_OPEN) {
486
+ ws.send(JSON.stringify({
487
+ ch: "terminal",
488
+ id,
489
+ type: "data",
490
+ data: payload.toString("utf-8"),
491
+ ...echo,
492
+ }));
493
+ }
494
+ if (msgType === 0x07) {
495
+ try {
496
+ const status = JSON.parse(payload.toString("utf-8"));
497
+ if (!status.alive && ws.readyState === WS_OPEN) {
498
+ ws.send(JSON.stringify({ ch: "terminal", id, type: "exited", code: 0, ...echo }));
499
+ }
500
+ }
501
+ catch {
502
+ /* ignore parse errors */
503
+ }
504
+ }
505
+ }
506
+ });
507
+ pipeSocket.on("close", () => {
508
+ winPipes.delete(pipeKey);
509
+ winPipeBuffers.delete(pipeKey);
510
+ if (ws.readyState === WS_OPEN) {
511
+ ws.send(JSON.stringify({ ch: "terminal", id, type: "exited", code: 0, ...echo }));
512
+ }
513
+ });
514
+ });
515
+ }
516
+ }
517
+ else if (type === "data" && msg.data !== undefined) {
518
+ const pipeSocket = winPipes.get(pipeKey);
519
+ if (pipeSocket) {
520
+ const inputBuf = Buffer.from(msg.data, "utf-8");
521
+ const header = Buffer.alloc(5);
522
+ header.writeUInt8(0x02, 0);
523
+ header.writeUInt32BE(inputBuf.length, 1);
524
+ pipeSocket.write(Buffer.concat([header, inputBuf]));
525
+ }
526
+ }
527
+ else if (type === "resize" && msg.cols !== undefined && msg.rows !== undefined) {
528
+ const pipeSocket = winPipes.get(pipeKey);
529
+ if (pipeSocket) {
530
+ const resizePayload = Buffer.from(JSON.stringify({ cols: msg.cols, rows: msg.rows }));
531
+ const header = Buffer.alloc(5);
532
+ header.writeUInt8(0x03, 0);
533
+ header.writeUInt32BE(resizePayload.length, 1);
534
+ pipeSocket.write(Buffer.concat([header, resizePayload]));
535
+ }
536
+ }
537
+ else if (type === "close") {
538
+ const pipeSocket = winPipes.get(pipeKey);
539
+ if (pipeSocket) {
540
+ pipeSocket.end();
541
+ winPipes.delete(pipeKey);
542
+ winPipeBuffers.delete(pipeKey);
543
+ }
544
+ }
545
+ }
381
546
  /**
382
547
  * Create a mux WebSocket server (noServer mode).
383
548
  * Returns the WebSocketServer instance for manual upgrade routing.
384
549
  */
385
550
  export function createMuxWebSocket(tmuxPath) {
386
- if (!ptySpawn) {
551
+ // On Windows, we use named pipe relay instead of node-pty/tmux.
552
+ // Allow the server to be created without ptySpawn on Windows.
553
+ if (!ptySpawn && !isWindows()) {
387
554
  console.warn("[MuxServer] node-pty not available — mux WebSocket will be disabled");
388
555
  return null;
389
556
  }
390
- const terminalManager = new TerminalManager(tmuxPath);
557
+ // On Windows, terminal I/O goes through named pipe relay — no TerminalManager needed.
558
+ const terminalManager = ptySpawn && !isWindows() ? new TerminalManager(tmuxPath ?? undefined) : null;
391
559
  const nextPort = process.env.PORT || "3000";
392
560
  const broadcaster = new SessionBroadcaster(nextPort);
393
561
  const wss = new WebSocketServer({ noServer: true });
394
562
  wss.on("connection", (ws) => {
395
563
  console.log("[MuxServer] New mux connection");
396
564
  const subscriptions = new Map();
565
+ // Windows: named pipe sockets keyed by session ID
566
+ const winPipes = new Map();
567
+ // Windows: framing buffers keyed by session ID
568
+ const winPipeBuffers = new Map();
397
569
  let sessionUnsubscribe = null;
398
570
  let missedPongs = 0;
399
571
  const MAX_MISSED_PONGS = 3;
@@ -433,71 +605,93 @@ export function createMuxWebSocket(tmuxPath) {
433
605
  const subscriptionKey = projectId ? `${projectId}:${id}` : id;
434
606
  try {
435
607
  if (type === "open") {
436
- // Validate session exists
437
- terminalManager.open(id, projectId, "tmuxName" in msg ? msg.tmuxName : undefined);
438
- // Send opened confirmation (idempotent — safe to send on re-open)
439
- const openedMsg = {
440
- ch: "terminal",
441
- id,
442
- type: "opened",
443
- ...(projectId && { projectId }),
444
- };
445
- ws.send(JSON.stringify(openedMsg));
446
- // Subscribe and send history buffer only for new subscribers.
447
- // Skipping the buffer on re-open prevents duplicate output when
448
- // MuxProvider re-sends open for all terminals on reconnect.
449
- if (!subscriptions.has(subscriptionKey)) {
450
- // Send buffered history to catch up the new subscriber
451
- const buffer = terminalManager.getBuffer(id, projectId);
452
- if (buffer) {
453
- const bufferMsg = {
454
- ch: "terminal",
455
- id,
456
- type: "data",
457
- data: buffer,
458
- ...(projectId && { projectId }),
459
- };
460
- ws.send(JSON.stringify(bufferMsg));
461
- }
462
- const unsub = terminalManager.subscribe(id, projectId, (data) => {
463
- const dataMsg = {
464
- ch: "terminal",
465
- id,
466
- type: "data",
467
- data,
468
- ...(projectId && { projectId }),
469
- };
470
- if (ws.readyState === WebSocket.OPEN) {
471
- ws.send(JSON.stringify(dataMsg));
472
- }
473
- }, (exitCode) => {
474
- const exitedMsg = {
475
- ch: "terminal",
476
- id,
477
- type: "exited",
478
- code: exitCode,
479
- ...(projectId && { projectId }),
480
- };
481
- if (ws.readyState === WebSocket.OPEN) {
482
- ws.send(JSON.stringify(exitedMsg));
608
+ if (isWindows()) {
609
+ handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
610
+ }
611
+ else {
612
+ // --- Unix: tmux path with project scoping ---
613
+ if (!terminalManager)
614
+ throw new Error("Terminal manager not available");
615
+ terminalManager.open(id, projectId, "tmuxName" in msg ? msg.tmuxName : undefined);
616
+ // Send opened confirmation (idempotent — safe to send on re-open)
617
+ const openedMsg = {
618
+ ch: "terminal",
619
+ id,
620
+ type: "opened",
621
+ ...(projectId && { projectId }),
622
+ };
623
+ ws.send(JSON.stringify(openedMsg));
624
+ // Subscribe and send history buffer only for new subscribers.
625
+ // Skipping the buffer on re-open prevents duplicate output when
626
+ // MuxProvider re-sends open for all terminals on reconnect.
627
+ if (!subscriptions.has(subscriptionKey)) {
628
+ // Send buffered history to catch up the new subscriber
629
+ const buffer = terminalManager.getBuffer(id, projectId);
630
+ if (buffer) {
631
+ const bufferMsg = {
632
+ ch: "terminal",
633
+ id,
634
+ type: "data",
635
+ data: buffer,
636
+ ...(projectId && { projectId }),
637
+ };
638
+ ws.send(JSON.stringify(bufferMsg));
483
639
  }
484
- });
485
- subscriptions.set(subscriptionKey, unsub);
640
+ const unsub = terminalManager.subscribe(id, projectId, (data) => {
641
+ const dataMsg = {
642
+ ch: "terminal",
643
+ id,
644
+ type: "data",
645
+ data,
646
+ ...(projectId && { projectId }),
647
+ };
648
+ if (ws.readyState === WebSocket.OPEN) {
649
+ ws.send(JSON.stringify(dataMsg));
650
+ }
651
+ }, (exitCode) => {
652
+ const exitedMsg = {
653
+ ch: "terminal",
654
+ id,
655
+ type: "exited",
656
+ code: exitCode,
657
+ ...(projectId && { projectId }),
658
+ };
659
+ if (ws.readyState === WebSocket.OPEN) {
660
+ ws.send(JSON.stringify(exitedMsg));
661
+ }
662
+ });
663
+ subscriptions.set(subscriptionKey, unsub);
664
+ }
486
665
  }
487
666
  }
488
667
  else if (type === "data" && "data" in msg) {
489
- terminalManager.write(id, msg.data, projectId);
668
+ if (isWindows()) {
669
+ handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
670
+ }
671
+ else {
672
+ terminalManager?.write(id, msg.data, projectId);
673
+ }
490
674
  }
491
675
  else if (type === "resize" && "cols" in msg && "rows" in msg) {
492
- terminalManager.resize(id, msg.cols, msg.rows, projectId);
676
+ if (isWindows()) {
677
+ handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
678
+ }
679
+ else {
680
+ terminalManager?.resize(id, msg.cols, msg.rows, projectId);
681
+ }
493
682
  }
494
683
  else if (type === "close") {
495
- // Unsubscribe this client only — TerminalManager is shared across
496
- // all mux connections so we must not kill the PTY here.
497
- const unsub = subscriptions.get(subscriptionKey);
498
- if (unsub) {
499
- unsub();
500
- subscriptions.delete(subscriptionKey);
684
+ if (isWindows()) {
685
+ handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
686
+ }
687
+ else {
688
+ // Unsubscribe this client only — TerminalManager is shared across
689
+ // all mux connections so we must not kill the PTY here.
690
+ const unsub = subscriptions.get(subscriptionKey);
691
+ if (unsub) {
692
+ unsub();
693
+ subscriptions.delete(subscriptionKey);
694
+ }
501
695
  }
502
696
  }
503
697
  }
@@ -558,6 +752,12 @@ export function createMuxWebSocket(tmuxPath) {
558
752
  unsub();
559
753
  }
560
754
  subscriptions.clear();
755
+ // Windows: close all open pipe sockets
756
+ for (const pipeSocket of winPipes.values()) {
757
+ pipeSocket.destroy();
758
+ }
759
+ winPipes.clear();
760
+ winPipeBuffers.clear();
561
761
  });
562
762
  // In the ws library, "error" is always followed by "close", so the close
563
763
  // handler below handles all cleanup. Log the error here and nothing more.
@@ -8,6 +8,7 @@ import { resolve, dirname } from "node:path";
8
8
  import { existsSync } from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { createRequire } from "node:module";
11
+ import { killProcessTree } from "@aoagents/ao-core";
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = dirname(__filename);
13
14
  // Resolve paths relative to the package root (one level up from dist-server/)
@@ -25,6 +26,7 @@ function spawnProcess(label, command, args, opts) {
25
26
  cwd: pkgRoot,
26
27
  stdio: ["ignore", "pipe", "pipe"],
27
28
  env: process.env,
29
+ detached: process.platform !== "win32",
28
30
  });
29
31
  child.stdout?.on("data", (data) => {
30
32
  for (const line of data.toString().split("\n").filter(Boolean)) {
@@ -60,10 +62,14 @@ function spawnProcess(label, command, args, opts) {
60
62
  * Tries the local .bin shim first (fast), then falls back to require.resolve (hoisted deps).
61
63
  */
62
64
  function resolveNextBin() {
63
- const localBin = resolve(pkgRoot, "node_modules", ".bin", "next");
64
- if (existsSync(localBin))
65
- return localBin;
66
- // Hoisted node_modules resolve the actual next CLI entry
65
+ // On Windows, .bin/next is a POSIX shell shim that spawn() cannot execute.
66
+ // Skip it and go straight to the JS entry point.
67
+ if (process.platform !== "win32") {
68
+ const localBin = resolve(pkgRoot, "node_modules", ".bin", "next");
69
+ if (existsSync(localBin))
70
+ return localBin;
71
+ }
72
+ // Resolve the actual Next.js CLI JS entry point
67
73
  const require = createRequire(resolve(pkgRoot, "package.json"));
68
74
  try {
69
75
  const nextPkg = require.resolve("next/package.json");
@@ -76,7 +82,15 @@ function resolveNextBin() {
76
82
  }
77
83
  // Start Next.js production server
78
84
  const port = process.env["PORT"] || "3000";
79
- spawnProcess("next", resolveNextBin(), ["start", "-p", port]);
85
+ const nextBin = resolveNextBin();
86
+ if (process.platform === "win32" && nextBin !== "next") {
87
+ // On Windows, run the JS entry point via the current node binary.
88
+ // spawn() can't execute .js files directly on Windows.
89
+ spawnProcess("next", process.execPath, [nextBin, "start", "-p", port]);
90
+ }
91
+ else {
92
+ spawnProcess("next", nextBin, ["start", "-p", port]);
93
+ }
80
94
  // Start direct terminal WebSocket server (auto-restart on crash)
81
95
  spawnProcess("direct-terminal", "node", [resolve(__dirname, "direct-terminal-ws.js")], { restart: true });
82
96
  // Graceful shutdown — send SIGTERM to children and wait for them to exit
@@ -104,7 +118,15 @@ function cleanup() {
104
118
  process.exit(0);
105
119
  }
106
120
  });
107
- child.kill("SIGTERM");
121
+ const pid = child.pid;
122
+ if (pid) {
123
+ void killProcessTree(pid, "SIGTERM").catch(() => {
124
+ child.kill("SIGTERM");
125
+ });
126
+ }
127
+ else {
128
+ child.kill("SIGTERM");
129
+ }
108
130
  }
109
131
  }
110
132
  process.on("SIGINT", cleanup);
@@ -4,10 +4,13 @@
4
4
  * Extracted from direct-terminal-ws.ts and terminal-websocket.ts
5
5
  * so the logic can be properly unit tested.
6
6
  */
7
- import { execFileSync } from "node:child_process";
8
- import { readdirSync, existsSync } from "node:fs";
7
+ import { execFile, execFileSync } from "node:child_process";
8
+ import { readdirSync, existsSync, readFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
+ import { promisify } from "node:util";
12
+ import { isWindows } from "@aoagents/ao-core";
13
+ const execFileAsync = promisify(execFile);
11
14
  /** Session ID validation regex — alphanumeric, hyphens, underscores only */
12
15
  export const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
13
16
  /** Hash prefix pattern — 12-char lowercase hex, as generated by generateConfigHash */
@@ -90,6 +93,8 @@ export function validateSessionId(sessionId) {
90
93
  * @param execFn - Injectable execFileSync for testing. Defaults to child_process.execFileSync.
91
94
  */
92
95
  export function findTmux(execFn = execFileSync) {
96
+ if (isWindows())
97
+ return null;
93
98
  const candidates = [
94
99
  "/opt/homebrew/bin/tmux", // macOS ARM (Homebrew)
95
100
  "/usr/local/bin/tmux", // macOS Intel (Homebrew)
@@ -106,6 +111,35 @@ export function findTmux(execFn = execFileSync) {
106
111
  }
107
112
  return "tmux"; // Fall back to bare name
108
113
  }
114
+ /**
115
+ * Check whether a tmux session with the given name exists.
116
+ *
117
+ * Uses `=` exact-match prefix so the lookup never falls back to tmux's
118
+ * default prefix matching (where "ao-1" would match "ao-15"). The caller
119
+ * must already have the canonical tmux session name (typically the value
120
+ * returned by `resolveTmuxSession`).
121
+ *
122
+ * Async: this runs from inside node-pty's `onExit` callback on every agent
123
+ * exit, and the WebSocket server is single-threaded. A synchronous
124
+ * `execFileSync` here would block the event loop — and every WebSocket
125
+ * connection, HTTP request, and in-flight terminal — for up to the 5 s
126
+ * subprocess timeout when tmux is slow to respond. Use the promisified
127
+ * `execFile` form instead.
128
+ *
129
+ * @returns true if the session exists, false otherwise (including tmux
130
+ * not running, no sessions, or any unexpected error)
131
+ */
132
+ export async function tmuxHasSession(tmuxPath, tmuxSessionName, execFn = execFileAsync) {
133
+ if (!tmuxPath)
134
+ return false;
135
+ try {
136
+ await execFn(tmuxPath, ["has-session", "-t", `=${tmuxSessionName}`], { timeout: 5000 });
137
+ return true;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
109
143
  /**
110
144
  * Resolve a user-facing session ID to its actual tmux session name.
111
145
  *
@@ -132,6 +166,8 @@ export function findTmux(execFn = execFileSync) {
132
166
  * @returns The actual tmux session name, or null if not found
133
167
  */
134
168
  export function resolveTmuxSession(sessionId, tmuxPath, execFn = execFileSync, fs = defaultFs, projectId) {
169
+ if (!tmuxPath)
170
+ return null;
135
171
  // Try exact match first using = prefix for exact matching (e.g., "ao-orchestrator")
136
172
  // Without =, tmux uses prefix matching: "ao-1" would match "ao-15"
137
173
  try {
@@ -177,3 +213,89 @@ export function resolveTmuxSession(sessionId, tmuxPath, execFn = execFileSync, f
177
213
  }
178
214
  return null;
179
215
  }
216
+ /**
217
+ * Resolve a user-facing session ID to its Windows named pipe path.
218
+ *
219
+ * V2 layout (current): JSON metadata at
220
+ * `~/.agent-orchestrator/projects/{projectId}/sessions/{sessionId}.json`
221
+ * with `runtimeHandle.data.pipePath` as a top-level field.
222
+ *
223
+ * V1 layout (legacy fallback): line-delimited key=value at
224
+ * `~/.agent-orchestrator/{storageKey}/sessions/{sessionId}` where
225
+ * storageKey is bare 12-hex or `{hash}-{projectName}`. Kept so users
226
+ * who haven't run `ao migrate-storage` still see live sessions.
227
+ *
228
+ * When `projectId` is provided, only that project's metadata file is read.
229
+ * Without it (legacy callers), walks all projects and returns the first
230
+ * matching pipePath — which can collide when two projects share a sessionId.
231
+ *
232
+ * @returns Full pipe path (e.g., "\\\\.\\pipe\\ao-pty-win1-orchestrator"), or null
233
+ */
234
+ export function resolvePipePath(sessionId, projectId, fs = defaultFs) {
235
+ if (!isWindows())
236
+ return null;
237
+ const readFile = fs.readFile ?? ((p) => readFileSync(p, "utf8"));
238
+ const aoBase = join(fs.homedir(), ".agent-orchestrator");
239
+ const readPipeFromV2 = (project) => {
240
+ const sessionFile = join(aoBase, "projects", project, "sessions", `${sessionId}.json`);
241
+ if (!fs.exists(sessionFile))
242
+ return null;
243
+ try {
244
+ const meta = JSON.parse(readFile(sessionFile));
245
+ const pipePath = meta.runtimeHandle?.data?.pipePath;
246
+ return typeof pipePath === "string" && pipePath.length > 0 ? pipePath : null;
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ };
252
+ // V2: prefer the caller's projectId when provided; otherwise walk all projects
253
+ const projectsDir = join(aoBase, "projects");
254
+ if (projectId) {
255
+ const pipe = readPipeFromV2(projectId);
256
+ if (pipe)
257
+ return pipe;
258
+ }
259
+ else if (fs.exists(projectsDir)) {
260
+ let projects;
261
+ try {
262
+ projects = fs.readdir(projectsDir);
263
+ }
264
+ catch {
265
+ projects = [];
266
+ }
267
+ for (const project of projects) {
268
+ const pipe = readPipeFromV2(project);
269
+ if (pipe)
270
+ return pipe;
271
+ }
272
+ }
273
+ // V1 fallback: line-delimited key=value under {storageKey}/sessions/{sessionId}
274
+ for (const storageKey of findStorageKeysForSession(sessionId, {
275
+ readdir: fs.readdir,
276
+ exists: fs.exists,
277
+ homedir: fs.homedir,
278
+ })) {
279
+ const sessionFile = join(aoBase, storageKey, "sessions", sessionId);
280
+ let content;
281
+ try {
282
+ content = readFile(sessionFile);
283
+ }
284
+ catch {
285
+ continue;
286
+ }
287
+ const match = content.match(/^runtimeHandle=(.+)$/m);
288
+ if (!match)
289
+ continue;
290
+ try {
291
+ const handle = JSON.parse(match[1]);
292
+ const pipePath = handle.data?.pipePath;
293
+ if (pipePath && pipePath.length > 0)
294
+ return pipePath;
295
+ }
296
+ catch {
297
+ continue;
298
+ }
299
+ }
300
+ return null;
301
+ }