vibepulse 0.2.2 → 0.3.0-beta.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 (253) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +1 -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 +8 -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/sessions/[id]/open-editor/route.js.nft.json +1 -1
  32. package/.next/server/app/api/node/sessions/route.js +5 -3
  33. package/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
  34. package/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
  35. package/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
  36. package/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
  37. package/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
  38. package/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
  39. package/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
  40. package/.next/server/app/api/profiles/route.js.nft.json +1 -1
  41. package/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
  42. package/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  43. package/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
  44. package/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
  45. package/.next/server/app/api/sessions/[id]/open-editor/route.js +1 -1
  46. package/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
  47. package/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
  48. package/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
  49. package/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
  50. package/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
  51. package/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
  52. package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
  53. package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
  54. package/.next/server/app/api/sessions/route.js +4 -2
  55. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  56. package/.next/server/app/index.html +1 -1
  57. package/.next/server/app/index.rsc +3 -3
  58. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  59. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  60. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  61. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  62. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/page_client-reference-manifest.js +1 -1
  64. package/.next/server/app-paths-manifest.json +1 -0
  65. package/.next/server/chunks/[root-of-the-server]__31d19c5c._.js +3 -0
  66. package/.next/server/chunks/[root-of-the-server]__31d19c5c._.js.map +1 -0
  67. package/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
  68. package/.next/server/chunks/[root-of-the-server]__56f5f249._.js.map +1 -1
  69. package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
  70. package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js.map +1 -0
  71. package/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
  72. package/.next/server/chunks/[root-of-the-server]__98073dd6._.js.map +1 -0
  73. package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js +3 -0
  74. package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js.map +1 -0
  75. package/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
  76. package/.next/server/chunks/[root-of-the-server]__d8e61048._.js.map +1 -1
  77. package/.next/server/chunks/[root-of-the-server]__f441109e._.js +3 -0
  78. package/.next/server/chunks/[root-of-the-server]__f441109e._.js.map +1 -0
  79. package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
  80. package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js.map +1 -0
  81. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js +3 -0
  82. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js.map +1 -0
  83. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
  84. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js.map +1 -0
  85. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
  86. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js.map +1 -1
  87. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
  88. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js.map +1 -1
  89. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js +3 -0
  90. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js.map +1 -0
  91. package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
  92. package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js.map +1 -0
  93. package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
  94. package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js.map → [root-of-the-server]__c91a8380._.js.map} +1 -1
  95. package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
  96. package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js.map +1 -1
  97. package/.next/server/pages/404.html +1 -1
  98. package/.next/server/pages/500.html +2 -2
  99. package/.next/server/server-reference-manifest.js +1 -1
  100. package/.next/server/server-reference-manifest.json +1 -1
  101. package/.next/standalone/.next/BUILD_ID +1 -1
  102. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  103. package/.next/standalone/.next/build-manifest.json +2 -2
  104. package/.next/standalone/.next/prerender-manifest.json +3 -3
  105. package/.next/standalone/.next/routes-manifest.json +8 -0
  106. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  107. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  108. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  109. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  110. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  111. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  112. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  113. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  114. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  115. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  116. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  117. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  118. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  119. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  120. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  121. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  122. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  123. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  124. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  125. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  126. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -1
  127. package/.next/standalone/.next/server/app/api/node/sessions/route.js +5 -3
  128. package/.next/standalone/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
  129. package/.next/standalone/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
  130. package/.next/standalone/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
  131. package/.next/standalone/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
  132. package/.next/standalone/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
  133. package/.next/standalone/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
  134. package/.next/standalone/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
  135. package/.next/standalone/.next/server/app/api/profiles/route.js.nft.json +1 -1
  136. package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
  137. package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  138. package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
  139. package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
  140. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js +1 -1
  141. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
  142. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
  143. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
  144. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
  145. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
  146. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
  147. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
  148. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
  149. package/.next/standalone/.next/server/app/api/sessions/route.js +4 -2
  150. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  151. package/.next/standalone/.next/server/app/index.html +1 -1
  152. package/.next/standalone/.next/server/app/index.rsc +3 -3
  153. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  154. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  155. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  156. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  157. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  158. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  159. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  160. package/.next/standalone/.next/server/chunks/[root-of-the-server]__31d19c5c._.js +3 -0
  161. package/.next/standalone/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
  162. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
  163. package/.next/standalone/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
  164. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b7b717eb._.js +3 -0
  165. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
  166. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f441109e._.js +3 -0
  167. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
  168. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js +3 -0
  169. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
  170. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
  171. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
  172. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js +3 -0
  173. package/.next/standalone/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
  174. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
  175. package/.next/standalone/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
  176. package/.next/standalone/.next/server/pages/404.html +1 -1
  177. package/.next/standalone/.next/server/pages/500.html +2 -2
  178. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  179. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  180. package/.next/standalone/.next/static/chunks/b3bc362202331708.css +3 -0
  181. package/.next/standalone/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
  182. package/.next/standalone/README.md +29 -5
  183. package/.next/standalone/docs/session-status-detection.md +36 -0
  184. package/.next/standalone/docs/superpowers/specs/2026-04-09-claude-capability-alignment-design.md +39 -0
  185. package/.next/standalone/package-lock.json +2 -2
  186. package/.next/standalone/package.json +1 -1
  187. package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.test.ts +60 -1
  188. package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.ts +77 -22
  189. package/.next/standalone/src/app/api/node/sessions/route.test.ts +282 -0
  190. package/.next/standalone/src/app/api/node/sessions/route.ts +141 -17
  191. package/.next/standalone/src/app/api/opencode-events/route.test.ts +3 -1
  192. package/.next/standalone/src/app/api/sessions/[id]/archive/route.test.ts +101 -0
  193. package/.next/standalone/src/app/api/sessions/[id]/archive/route.ts +47 -12
  194. package/.next/standalone/src/app/api/sessions/[id]/delete/route.test.ts +92 -0
  195. package/.next/standalone/src/app/api/sessions/[id]/delete/route.ts +45 -10
  196. package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.test.ts +74 -0
  197. package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.ts +22 -2
  198. package/.next/standalone/src/app/api/sessions/[id]/restore/route.test.ts +186 -0
  199. package/.next/standalone/src/app/api/sessions/[id]/restore/route.ts +184 -0
  200. package/.next/standalone/src/app/api/sessions/route.test.ts +1889 -107
  201. package/.next/standalone/src/app/api/sessions/route.ts +365 -981
  202. package/.next/standalone/src/components/KanbanBoard.test.tsx +307 -1
  203. package/.next/standalone/src/components/KanbanBoard.tsx +105 -18
  204. package/.next/standalone/src/components/ProjectCard.test.tsx +416 -2
  205. package/.next/standalone/src/components/ProjectCard.tsx +238 -86
  206. package/.next/standalone/src/components/SessionCard.test.tsx +253 -2
  207. package/.next/standalone/src/components/SessionCard.tsx +182 -76
  208. package/.next/standalone/src/hooks/useOpencodeSync.test.ts +321 -1
  209. package/.next/standalone/src/hooks/useOpencodeSync.ts +16 -12
  210. package/.next/standalone/src/lib/claudeSessionOverrides.test.ts +75 -0
  211. package/.next/standalone/src/lib/claudeSessionOverrides.ts +169 -0
  212. package/.next/standalone/src/lib/session-providers/claudeCode.test.ts +2288 -0
  213. package/.next/standalone/src/lib/session-providers/claudeCode.ts +1083 -0
  214. package/.next/standalone/src/lib/session-providers/localAggregator.test.ts +322 -0
  215. package/.next/standalone/src/lib/session-providers/localAggregator.ts +302 -0
  216. package/.next/standalone/src/lib/session-providers/opencodeProvider.ts +723 -0
  217. package/.next/standalone/src/lib/session-providers/providerIds.test.ts +337 -0
  218. package/.next/standalone/src/lib/session-providers/providerIds.ts +176 -0
  219. package/.next/standalone/src/lib/session-providers/types.ts +131 -0
  220. package/.next/standalone/src/lib/transform.test.ts +253 -0
  221. package/.next/standalone/src/lib/transform.ts +96 -37
  222. package/.next/standalone/src/types/index.ts +23 -17
  223. package/.next/static/chunks/b3bc362202331708.css +3 -0
  224. package/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
  225. package/.next/trace +1 -1
  226. package/.next/trace-build +1 -1
  227. package/.next/types/routes.d.ts +2 -1
  228. package/.next/types/validator.ts +9 -0
  229. package/README.md +29 -5
  230. package/package.json +1 -1
  231. package/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
  232. package/.next/server/chunks/[root-of-the-server]__2f981540._.js.map +0 -1
  233. package/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
  234. package/.next/server/chunks/[root-of-the-server]__3745b314._.js.map +0 -1
  235. package/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
  236. package/.next/server/chunks/[root-of-the-server]__6c428a24._.js.map +0 -1
  237. package/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
  238. package/.next/server/chunks/[root-of-the-server]__73a00b88._.js.map +0 -1
  239. package/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
  240. package/.next/server/chunks/[root-of-the-server]__db285678._.js.map +0 -1
  241. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
  243. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
  245. package/.next/standalone/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
  246. package/.next/standalone/.next/static/chunks/f42202943f6742e5.css +0 -3
  247. package/.next/static/chunks/f42202943f6742e5.css +0 -3
  248. /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
  249. /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
  250. /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
  251. /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
  252. /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
  253. /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
@@ -1,11 +1,13 @@
1
1
  import { discoverOpencodePortsWithMeta } from '@/lib/opencodeDiscovery';
2
- import { parseActionSessionReference, resolveLocalActionSessionId } from '@/lib/hostIdentity';
2
+ import { ActionSessionReference, parseActionSessionReference, resolveLocalActionSessionId } from '@/lib/hostIdentity';
3
3
  import { listNodeRecords } from '@/lib/nodeRegistry';
4
4
  import { createNodeRequestHeaders } from '@/lib/nodeProtocol';
5
+ import { detectProviderFromRawId, extractProviderRawId, getDefaultProviderContext } from '@/lib/session-providers/providerIds';
5
6
  import {
6
- clearSessionForceUnarchived,
7
- markSessionStickyStatusBlocked,
7
+ clearSessionForceUnarchived,
8
+ markSessionStickyStatusBlocked,
8
9
  } from '@/lib/sessionArchiveOverrides';
10
+ import { markClaudeSessionArchived } from '@/lib/claudeSessionOverrides';
9
11
 
10
12
  const REMOTE_NODE_ACTION_TIMEOUT_MS = 5_000;
11
13
 
@@ -23,6 +25,20 @@ function createSessionNotFoundResponse() {
23
25
  );
24
26
  }
25
27
 
28
+ function createUnsupportedCapabilityResponse(capability: 'archive', sessionId: string) {
29
+ const provider = detectProviderFromRawId(sessionId);
30
+
31
+ return Response.json(
32
+ {
33
+ error: 'Session action not supported by provider',
34
+ reason: 'provider_capability_unsupported',
35
+ provider,
36
+ capability,
37
+ },
38
+ { status: 403 }
39
+ );
40
+ }
41
+
26
42
  function createArchiveFailureResponse(
27
43
  status: number,
28
44
  message?: string,
@@ -87,17 +103,36 @@ async function forwardRemoteArchive(hostId: string, sessionId: string): Promise<
87
103
 
88
104
  export async function POST(_: Request, { params }: { params: Promise<{ id: string }> }) {
89
105
  const { id } = await params;
90
- const sessionId = resolveLocalActionSessionId(id);
106
+ let actionTarget: ActionSessionReference;
91
107
 
92
- if (!sessionId) {
93
- try {
94
- const actionTarget = parseActionSessionReference(id);
95
- if (actionTarget.isRemote) {
96
- return forwardRemoteArchive(actionTarget.hostId, actionTarget.sessionId);
97
- }
98
- } catch {
99
- return createInvalidActionSessionIdResponse();
108
+ try {
109
+ actionTarget = parseActionSessionReference(id);
110
+ } catch {
111
+ return createInvalidActionSessionIdResponse();
112
+ }
113
+
114
+ const provider = detectProviderFromRawId(actionTarget.sessionId);
115
+ if (!getDefaultProviderContext(provider).capabilities.archive) {
116
+ return createUnsupportedCapabilityResponse('archive', actionTarget.sessionId);
117
+ }
118
+
119
+ if (provider === 'claude-code' && actionTarget.isRemote) {
120
+ return createUnsupportedCapabilityResponse('archive', actionTarget.sessionId);
121
+ }
122
+
123
+ if (provider === 'claude-code') {
124
+ await markClaudeSessionArchived(extractProviderRawId(actionTarget.sessionId));
125
+ const localSessionId = resolveLocalActionSessionId(id);
126
+ if (localSessionId) {
127
+ clearSessionForceUnarchived(localSessionId);
128
+ markSessionStickyStatusBlocked(localSessionId);
100
129
  }
130
+ return Response.json({ success: true });
131
+ }
132
+
133
+ const sessionId = resolveLocalActionSessionId(id);
134
+ if (!sessionId && actionTarget.isRemote) {
135
+ return forwardRemoteArchive(actionTarget.hostId, actionTarget.sessionId);
101
136
  }
102
137
 
103
138
  if (!sessionId) {
@@ -17,15 +17,21 @@ vi.mock('@/lib/sessionArchiveOverrides', () => ({
17
17
  clearSessionStickyStatusBlocked: vi.fn(),
18
18
  }));
19
19
 
20
+ vi.mock('@/lib/claudeSessionOverrides', () => ({
21
+ markClaudeSessionDeleted: vi.fn(),
22
+ }));
23
+
20
24
  import { createOpencodeClient } from '@opencode-ai/sdk';
21
25
  import { discoverOpencodePortsWithMeta } from '@/lib/opencodeDiscovery';
22
26
  import { listNodeRecords } from '@/lib/nodeRegistry';
27
+ import { markClaudeSessionDeleted } from '@/lib/claudeSessionOverrides';
23
28
 
24
29
  import { POST } from './route';
25
30
 
26
31
  const mockCreateOpencodeClient: any = createOpencodeClient;
27
32
  const mockDiscoverPortsWithMeta: any = discoverOpencodePortsWithMeta;
28
33
  const mockListNodeRecords: any = listNodeRecords;
34
+ const mockMarkClaudeSessionDeleted: any = markClaudeSessionDeleted;
29
35
  const mockSessionDelete = vi.fn();
30
36
 
31
37
  describe('/api/sessions/[id]/delete', () => {
@@ -110,4 +116,90 @@ describe('/api/sessions/[id]/delete', () => {
110
116
  message: '404 not found',
111
117
  });
112
118
  });
119
+
120
+ it('deletes Claude sessions through local override storage before any OpenCode execution', async () => {
121
+ const mockFetch = vi.fn();
122
+ vi.stubGlobal('fetch', mockFetch);
123
+
124
+ const response = await POST(new Request('http://localhost/api/sessions/local:claude~550e8400-e29b-41d4-a716-446655440000/delete', { method: 'POST' }), {
125
+ params: Promise.resolve({ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000' }),
126
+ });
127
+ const data = await response.json();
128
+
129
+ expect(response.status).toBe(200);
130
+ expect(data).toEqual({ success: true });
131
+ expect(mockMarkClaudeSessionDeleted).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
132
+ expect(mockDiscoverPortsWithMeta).not.toHaveBeenCalled();
133
+ expect(mockCreateOpencodeClient).not.toHaveBeenCalled();
134
+ expect(mockSessionDelete).not.toHaveBeenCalled();
135
+ expect(mockFetch).not.toHaveBeenCalled();
136
+ });
137
+
138
+ it('deletes scoped Claude sidechain sessions through local override storage', async () => {
139
+ const scopedSessionId = '550e8400-e29b-41d4-a716-446655440000__agent-a123';
140
+ const mockFetch = vi.fn();
141
+ vi.stubGlobal('fetch', mockFetch);
142
+
143
+ const response = await POST(new Request(`http://localhost/api/sessions/local:${scopedSessionId}/delete`, { method: 'POST' }), {
144
+ params: Promise.resolve({ id: `local:${scopedSessionId}` }),
145
+ });
146
+ const data = await response.json();
147
+
148
+ expect(response.status).toBe(200);
149
+ expect(data).toEqual({ success: true });
150
+ expect(mockMarkClaudeSessionDeleted).toHaveBeenCalledWith(scopedSessionId);
151
+ expect(mockDiscoverPortsWithMeta).not.toHaveBeenCalled();
152
+ expect(mockCreateOpencodeClient).not.toHaveBeenCalled();
153
+ expect(mockSessionDelete).not.toHaveBeenCalled();
154
+ expect(mockFetch).not.toHaveBeenCalled();
155
+ });
156
+
157
+ it('rejects remote Claude delete requests before local override or node execution', async () => {
158
+ const mockFetch = vi.fn();
159
+ vi.stubGlobal('fetch', mockFetch);
160
+
161
+ const response = await POST(new Request('http://localhost/api/sessions/node-1:claude~550e8400-e29b-41d4-a716-446655440000/delete', { method: 'POST' }), {
162
+ params: Promise.resolve({ id: 'node-1:claude~550e8400-e29b-41d4-a716-446655440000' }),
163
+ });
164
+ const data = await response.json();
165
+
166
+ expect(response.status).toBe(403);
167
+ expect(data).toEqual({
168
+ error: 'Session action not supported by provider',
169
+ reason: 'provider_capability_unsupported',
170
+ provider: 'claude-code',
171
+ capability: 'delete',
172
+ });
173
+ expect(mockMarkClaudeSessionDeleted).not.toHaveBeenCalled();
174
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
175
+ expect(mockDiscoverPortsWithMeta).not.toHaveBeenCalled();
176
+ expect(mockCreateOpencodeClient).not.toHaveBeenCalled();
177
+ expect(mockSessionDelete).not.toHaveBeenCalled();
178
+ expect(mockFetch).not.toHaveBeenCalled();
179
+ });
180
+
181
+ it('rejects remote scoped Claude sidechain deletes before node execution', async () => {
182
+ const scopedSessionId = '550e8400-e29b-41d4-a716-446655440000__agent-a123';
183
+ const mockFetch = vi.fn();
184
+ vi.stubGlobal('fetch', mockFetch);
185
+
186
+ const response = await POST(new Request(`http://localhost/api/sessions/node-1:${scopedSessionId}/delete`, { method: 'POST' }), {
187
+ params: Promise.resolve({ id: `node-1:${scopedSessionId}` }),
188
+ });
189
+ const data = await response.json();
190
+
191
+ expect(response.status).toBe(403);
192
+ expect(data).toEqual({
193
+ error: 'Session action not supported by provider',
194
+ reason: 'provider_capability_unsupported',
195
+ provider: 'claude-code',
196
+ capability: 'delete',
197
+ });
198
+ expect(mockMarkClaudeSessionDeleted).not.toHaveBeenCalled();
199
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
200
+ expect(mockDiscoverPortsWithMeta).not.toHaveBeenCalled();
201
+ expect(mockCreateOpencodeClient).not.toHaveBeenCalled();
202
+ expect(mockSessionDelete).not.toHaveBeenCalled();
203
+ expect(mockFetch).not.toHaveBeenCalled();
204
+ });
113
205
  });
@@ -1,12 +1,14 @@
1
1
  import { createOpencodeClient } from '@opencode-ai/sdk';
2
2
  import { discoverOpencodePortsWithMeta } from '@/lib/opencodeDiscovery';
3
- import { parseActionSessionReference, resolveLocalActionSessionId } from '@/lib/hostIdentity';
3
+ import { ActionSessionReference, parseActionSessionReference, resolveLocalActionSessionId } from '@/lib/hostIdentity';
4
4
  import { listNodeRecords } from '@/lib/nodeRegistry';
5
5
  import { createNodeRequestHeaders } from '@/lib/nodeProtocol';
6
+ import { detectProviderFromRawId, extractProviderRawId, getDefaultProviderContext } from '@/lib/session-providers/providerIds';
6
7
  import {
7
8
  clearSessionForceUnarchived,
8
9
  clearSessionStickyStatusBlocked,
9
10
  } from '@/lib/sessionArchiveOverrides';
11
+ import { markClaudeSessionDeleted } from '@/lib/claudeSessionOverrides';
10
12
 
11
13
  const REMOTE_NODE_ACTION_TIMEOUT_MS = 5_000;
12
14
 
@@ -28,6 +30,20 @@ function createSessionNotFoundResponse(message?: string) {
28
30
  );
29
31
  }
30
32
 
33
+ function createUnsupportedCapabilityResponse(capability: 'delete', sessionId: string) {
34
+ const provider = detectProviderFromRawId(sessionId);
35
+
36
+ return Response.json(
37
+ {
38
+ error: 'Session action not supported by provider',
39
+ reason: 'provider_capability_unsupported',
40
+ provider,
41
+ capability,
42
+ },
43
+ { status: 403 }
44
+ );
45
+ }
46
+
31
47
  async function forwardRemoteDelete(hostId: string, sessionId: string): Promise<Response> {
32
48
  const nodeRecords = await listNodeRecords();
33
49
  const nodeRecord = nodeRecords.find((node) => node.nodeId === hostId);
@@ -77,17 +93,36 @@ async function forwardRemoteDelete(hostId: string, sessionId: string): Promise<R
77
93
 
78
94
  export async function POST(_: Request, { params }: { params: Promise<{ id: string }> }) {
79
95
  const { id } = await params;
80
- const sessionId = resolveLocalActionSessionId(id);
96
+ let actionTarget: ActionSessionReference;
81
97
 
82
- if (!sessionId) {
83
- try {
84
- const actionTarget = parseActionSessionReference(id);
85
- if (actionTarget.isRemote) {
86
- return forwardRemoteDelete(actionTarget.hostId, actionTarget.sessionId);
87
- }
88
- } catch {
89
- return createInvalidActionSessionIdResponse();
98
+ try {
99
+ actionTarget = parseActionSessionReference(id);
100
+ } catch {
101
+ return createInvalidActionSessionIdResponse();
102
+ }
103
+
104
+ const provider = detectProviderFromRawId(actionTarget.sessionId);
105
+ if (!getDefaultProviderContext(provider).capabilities.delete) {
106
+ return createUnsupportedCapabilityResponse('delete', actionTarget.sessionId);
107
+ }
108
+
109
+ if (provider === 'claude-code' && actionTarget.isRemote) {
110
+ return createUnsupportedCapabilityResponse('delete', actionTarget.sessionId);
111
+ }
112
+
113
+ if (provider === 'claude-code') {
114
+ await markClaudeSessionDeleted(extractProviderRawId(actionTarget.sessionId));
115
+ const localSessionId = resolveLocalActionSessionId(id);
116
+ if (localSessionId) {
117
+ clearSessionForceUnarchived(localSessionId);
118
+ clearSessionStickyStatusBlocked(localSessionId);
90
119
  }
120
+ return Response.json({ success: true });
121
+ }
122
+
123
+ const sessionId = resolveLocalActionSessionId(id);
124
+ if (!sessionId && actionTarget.isRemote) {
125
+ return forwardRemoteDelete(actionTarget.hostId, actionTarget.sessionId);
91
126
  }
92
127
 
93
128
  if (!sessionId) {
@@ -215,4 +215,78 @@ describe('/api/sessions/[id]/open-editor', () => {
215
215
  expect(response.status).toBe(400);
216
216
  expect(data).toEqual({ error: 'Invalid action session id', reason: 'invalid_action_session_id' });
217
217
  });
218
+
219
+ it('rejects unsupported Claude open-editor requests before remote node execution', async () => {
220
+ const mockFetch = vi.fn();
221
+ vi.stubGlobal('fetch', mockFetch);
222
+
223
+ const response = await POST(
224
+ new Request('http://localhost/api/sessions/node-1:claude~550e8400-e29b-41d4-a716-446655440000/open-editor', {
225
+ method: 'POST',
226
+ headers: { 'Content-Type': 'application/json' },
227
+ body: JSON.stringify({ tool: 'vscode' }),
228
+ }),
229
+ { params: Promise.resolve({ id: 'node-1:claude~550e8400-e29b-41d4-a716-446655440000' }) }
230
+ );
231
+ const data = await response.json();
232
+
233
+ expect(response.status).toBe(403);
234
+ expect(data).toEqual({
235
+ error: 'Session action not supported by provider',
236
+ reason: 'provider_capability_unsupported',
237
+ provider: 'claude-code',
238
+ capability: 'openEditor',
239
+ });
240
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
241
+ expect(mockFetch).not.toHaveBeenCalled();
242
+ });
243
+
244
+ it('rejects unsupported scoped Claude sidechain open-editor requests before remote node execution', async () => {
245
+ const scopedSessionId = '550e8400-e29b-41d4-a716-446655440000__agent-a123';
246
+ const mockFetch = vi.fn();
247
+ vi.stubGlobal('fetch', mockFetch);
248
+
249
+ const response = await POST(
250
+ new Request(`http://localhost/api/sessions/node-1:${scopedSessionId}/open-editor`, {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify({ tool: 'vscode' }),
254
+ }),
255
+ { params: Promise.resolve({ id: `node-1:${scopedSessionId}` }) }
256
+ );
257
+ const data = await response.json();
258
+
259
+ expect(response.status).toBe(403);
260
+ expect(data).toEqual({
261
+ error: 'Session action not supported by provider',
262
+ reason: 'provider_capability_unsupported',
263
+ provider: 'claude-code',
264
+ capability: 'openEditor',
265
+ });
266
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
267
+ expect(mockFetch).not.toHaveBeenCalled();
268
+ });
269
+
270
+ it('rejects local scoped Claude sidechain open-editor requests as unsupported capability', async () => {
271
+ const scopedSessionId = '550e8400-e29b-41d4-a716-446655440000__agent-a123';
272
+
273
+ const response = await POST(
274
+ new Request(`http://localhost/api/sessions/local:${scopedSessionId}/open-editor`, {
275
+ method: 'POST',
276
+ headers: { 'Content-Type': 'application/json' },
277
+ body: JSON.stringify({ tool: 'vscode' }),
278
+ }),
279
+ { params: Promise.resolve({ id: `local:${scopedSessionId}` }) }
280
+ );
281
+ const data = await response.json();
282
+
283
+ expect(response.status).toBe(403);
284
+ expect(data).toEqual({
285
+ error: 'Session action not supported by provider',
286
+ reason: 'provider_capability_unsupported',
287
+ provider: 'claude-code',
288
+ capability: 'openEditor',
289
+ });
290
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
291
+ });
218
292
  });
@@ -1,6 +1,7 @@
1
- import { parseActionSessionReference } from '@/lib/hostIdentity';
1
+ import { ActionSessionReference, parseActionSessionReference } from '@/lib/hostIdentity';
2
2
  import { listNodeRecords } from '@/lib/nodeRegistry';
3
3
  import { createNodeRequestHeaders } from '@/lib/nodeProtocol';
4
+ import { detectProviderFromRawId, extractProviderRawId, getDefaultProviderContext } from '@/lib/session-providers/providerIds';
4
5
 
5
6
  const REMOTE_NODE_ACTION_TIMEOUT_MS = 5_000;
6
7
 
@@ -18,16 +19,35 @@ function createSessionNotFoundResponse() {
18
19
  );
19
20
  }
20
21
 
22
+ function createUnsupportedCapabilityResponse(capability: 'openEditor', sessionId: string) {
23
+ const provider = detectProviderFromRawId(sessionId);
24
+
25
+ return Response.json(
26
+ {
27
+ error: 'Session action not supported by provider',
28
+ reason: 'provider_capability_unsupported',
29
+ provider,
30
+ capability,
31
+ },
32
+ { status: 403 }
33
+ );
34
+ }
35
+
21
36
  export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
22
37
  const { id } = await params;
23
38
 
24
- let actionTarget;
39
+ let actionTarget: ActionSessionReference;
25
40
  try {
26
41
  actionTarget = parseActionSessionReference(id);
27
42
  } catch {
28
43
  return createInvalidActionSessionIdResponse();
29
44
  }
30
45
 
46
+ const provider = detectProviderFromRawId(actionTarget.sessionId);
47
+ if (!getDefaultProviderContext(provider).capabilities.openEditor) {
48
+ return createUnsupportedCapabilityResponse('openEditor', actionTarget.sessionId);
49
+ }
50
+
31
51
  if (!actionTarget.isRemote) {
32
52
  return createInvalidActionSessionIdResponse();
33
53
  }
@@ -0,0 +1,186 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('@/lib/opencodeDiscovery', () => ({
4
+ discoverOpencodePortsWithMeta: vi.fn(() => ({ ports: [], timedOut: false })),
5
+ }));
6
+
7
+ vi.mock('@/lib/nodeRegistry', () => ({
8
+ listNodeRecords: vi.fn(async () => []),
9
+ }));
10
+
11
+ vi.mock('@/lib/sessionArchiveOverrides', () => ({
12
+ markSessionForceUnarchived: vi.fn(),
13
+ clearSessionStickyStatusBlocked: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('@/lib/claudeSessionOverrides', () => ({
17
+ clearClaudeSessionArchived: vi.fn(),
18
+ }));
19
+
20
+ import { POST } from './route';
21
+ import { discoverOpencodePortsWithMeta } from '@/lib/opencodeDiscovery';
22
+ import { listNodeRecords } from '@/lib/nodeRegistry';
23
+ import { clearClaudeSessionArchived } from '@/lib/claudeSessionOverrides';
24
+ import {
25
+ clearSessionStickyStatusBlocked,
26
+ markSessionForceUnarchived,
27
+ } from '@/lib/sessionArchiveOverrides';
28
+
29
+ const mockDiscoverOpencodePortsWithMeta: any = discoverOpencodePortsWithMeta;
30
+ const mockListNodeRecords: any = listNodeRecords;
31
+ const mockClearClaudeSessionArchived: any = clearClaudeSessionArchived;
32
+ const mockMarkSessionForceUnarchived: any = markSessionForceUnarchived;
33
+ const mockClearSessionStickyStatusBlocked: any = clearSessionStickyStatusBlocked;
34
+
35
+ describe('/api/sessions/[id]/restore', () => {
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ mockDiscoverOpencodePortsWithMeta.mockReturnValue({ ports: [], timedOut: false });
39
+ mockListNodeRecords.mockResolvedValue([]);
40
+ });
41
+
42
+ it('restores Claude sessions through local override storage', async () => {
43
+ const response = await POST(new Request('http://localhost/api/sessions/local:claude~550e8400-e29b-41d4-a716-446655440000/restore', { method: 'POST' }), {
44
+ params: Promise.resolve({ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000' }),
45
+ });
46
+ const data = await response.json();
47
+
48
+ expect(response.status).toBe(200);
49
+ expect(data).toEqual({ success: true });
50
+ expect(mockMarkSessionForceUnarchived).toHaveBeenCalledWith('claude~550e8400-e29b-41d4-a716-446655440000');
51
+ expect(mockClearSessionStickyStatusBlocked).toHaveBeenCalledWith('claude~550e8400-e29b-41d4-a716-446655440000');
52
+ expect(mockClearClaudeSessionArchived).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
53
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
54
+ expect(mockDiscoverOpencodePortsWithMeta).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it('restores scoped Claude sidechain sessions through local override storage', async () => {
58
+ const scopedSessionId = '550e8400-e29b-41d4-a716-446655440000__agent-a123';
59
+ const response = await POST(new Request(`http://localhost/api/sessions/local:${scopedSessionId}/restore`, { method: 'POST' }), {
60
+ params: Promise.resolve({ id: `local:${scopedSessionId}` }),
61
+ });
62
+ const data = await response.json();
63
+
64
+ expect(response.status).toBe(200);
65
+ expect(data).toEqual({ success: true });
66
+ expect(mockMarkSessionForceUnarchived).toHaveBeenCalledWith(scopedSessionId);
67
+ expect(mockClearSessionStickyStatusBlocked).toHaveBeenCalledWith(scopedSessionId);
68
+ expect(mockClearClaudeSessionArchived).toHaveBeenCalledWith(scopedSessionId);
69
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
70
+ expect(mockDiscoverOpencodePortsWithMeta).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it('rejects remote Claude restore requests before local override or node execution', async () => {
74
+ const mockFetch = vi.fn();
75
+ vi.stubGlobal('fetch', mockFetch);
76
+
77
+ const response = await POST(new Request('http://localhost/api/sessions/node-1:claude~550e8400-e29b-41d4-a716-446655440000/restore', { method: 'POST' }), {
78
+ params: Promise.resolve({ id: 'node-1:claude~550e8400-e29b-41d4-a716-446655440000' }),
79
+ });
80
+ const data = await response.json();
81
+
82
+ expect(response.status).toBe(403);
83
+ expect(data).toEqual({
84
+ error: 'Session action not supported by provider',
85
+ reason: 'provider_capability_unsupported',
86
+ provider: 'claude-code',
87
+ capability: 'archive',
88
+ });
89
+ expect(mockClearClaudeSessionArchived).not.toHaveBeenCalled();
90
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
91
+ expect(mockDiscoverOpencodePortsWithMeta).not.toHaveBeenCalled();
92
+ expect(mockFetch).not.toHaveBeenCalled();
93
+ });
94
+
95
+ it('rejects remote scoped Claude sidechain restore requests before node execution', async () => {
96
+ const scopedSessionId = '550e8400-e29b-41d4-a716-446655440000__agent-a123';
97
+ const mockFetch = vi.fn();
98
+ vi.stubGlobal('fetch', mockFetch);
99
+
100
+ const response = await POST(new Request(`http://localhost/api/sessions/node-1:${scopedSessionId}/restore`, { method: 'POST' }), {
101
+ params: Promise.resolve({ id: `node-1:${scopedSessionId}` }),
102
+ });
103
+ const data = await response.json();
104
+
105
+ expect(response.status).toBe(403);
106
+ expect(data).toEqual({
107
+ error: 'Session action not supported by provider',
108
+ reason: 'provider_capability_unsupported',
109
+ provider: 'claude-code',
110
+ capability: 'archive',
111
+ });
112
+ expect(mockClearClaudeSessionArchived).not.toHaveBeenCalled();
113
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
114
+ expect(mockDiscoverOpencodePortsWithMeta).not.toHaveBeenCalled();
115
+ expect(mockFetch).not.toHaveBeenCalled();
116
+ });
117
+
118
+ it('forwards remote opencode restores to node archive DELETE endpoint', async () => {
119
+ mockListNodeRecords.mockResolvedValue([
120
+ {
121
+ nodeId: 'node-1',
122
+ nodeLabel: 'Node 1',
123
+ baseUrl: 'https://node-1.test',
124
+ enabled: true,
125
+ token: 'node-token',
126
+ createdAt: new Date().toISOString(),
127
+ updatedAt: new Date().toISOString(),
128
+ },
129
+ ]);
130
+
131
+ const mockFetch = vi.fn(async () => new Response(JSON.stringify({ success: true }), { status: 200 }));
132
+ vi.stubGlobal('fetch', mockFetch);
133
+
134
+ const response = await POST(new Request('http://localhost/api/sessions/node-1:abc/restore', { method: 'POST' }), {
135
+ params: Promise.resolve({ id: 'node-1:abc' }),
136
+ });
137
+ const data = await response.json();
138
+
139
+ expect(response.status).toBe(200);
140
+ expect(data).toEqual({ success: true });
141
+ expect(mockFetch).toHaveBeenCalledWith('https://node-1.test/api/node/sessions/abc/archive', expect.objectContaining({ method: 'DELETE' }));
142
+ expect(mockClearClaudeSessionArchived).not.toHaveBeenCalled();
143
+ expect(mockDiscoverOpencodePortsWithMeta).not.toHaveBeenCalled();
144
+ });
145
+
146
+ it('clears sticky blocked state when local opencode restore succeeds', async () => {
147
+ mockDiscoverOpencodePortsWithMeta.mockReturnValue({ ports: [3456], timedOut: false });
148
+ const mockFetch = vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 }));
149
+ vi.stubGlobal('fetch', mockFetch);
150
+
151
+ const response = await POST(new Request('http://localhost/api/sessions/local:session-1/restore', { method: 'POST' }), {
152
+ params: Promise.resolve({ id: 'local:session-1' }),
153
+ });
154
+ const data = await response.json();
155
+
156
+ expect(response.status).toBe(200);
157
+ expect(data).toEqual({ success: true });
158
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:3456/session/session-1', expect.objectContaining({ method: 'PATCH' }));
159
+ expect(mockMarkSessionForceUnarchived).toHaveBeenCalledWith('session-1');
160
+ expect(mockClearSessionStickyStatusBlocked).toHaveBeenCalledWith('session-1');
161
+ });
162
+
163
+ it('returns the latest non-404 upstream failure when restore attempts fail', async () => {
164
+ mockDiscoverOpencodePortsWithMeta.mockReturnValue({ ports: [3456, 3457], timedOut: false });
165
+ const mockFetch = vi.fn(async (input: RequestInfo | URL) => {
166
+ const url = String(input);
167
+ if (url.includes(':3456/')) {
168
+ return new Response('upstream boom', { status: 503 });
169
+ }
170
+ return new Response('not found', { status: 404 });
171
+ });
172
+ vi.stubGlobal('fetch', mockFetch);
173
+
174
+ const response = await POST(new Request('http://localhost/api/sessions/local:session-2/restore', { method: 'POST' }), {
175
+ params: Promise.resolve({ id: 'local:session-2' }),
176
+ });
177
+ const data = await response.json();
178
+
179
+ expect(response.status).toBe(503);
180
+ expect(data).toEqual(expect.objectContaining({
181
+ error: 'Failed to restore session',
182
+ reason: 'restore_request_failed',
183
+ message: 'upstream boom',
184
+ }));
185
+ });
186
+ });