vibepulse 0.2.2 → 0.3.1-beta
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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +2 -2
- package/.next/cache/.previewinfo +1 -1
- package/.next/cache/.rscinfo +1 -1
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/fallback-build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/routes-manifest.json +8 -0
- package/.next/server/app/_global-error/page.js +1 -1
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/node/events/route.js +2 -2
- package/.next/server/app/api/node/events/route.js.nft.json +1 -1
- package/.next/server/app/api/node/sessions/[id]/delete/route.js +2 -2
- package/.next/server/app/api/node/sessions/[id]/delete/route.js.nft.json +1 -1
- package/.next/server/app/api/node/sessions/[id]/open-editor/route.js +2 -2
- package/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/server/app/api/node/sessions/route.js +6 -4
- package/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/nodes/route.js +2 -2
- package/.next/server/app/api/nodes/route.js.nft.json +1 -1
- package/.next/server/app/api/opencode-config/route.js +2 -2
- package/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
- package/.next/server/app/api/opencode-config/status/route.js +2 -2
- package/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
- package/.next/server/app/api/opencode-events/route.js +3 -3
- package/.next/server/app/api/opencode-events/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/apply/route.js +2 -2
- package/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/export/route.js +2 -2
- package/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/route.js +2 -2
- package/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/import/route.js +2 -2
- package/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/route.js +2 -2
- package/.next/server/app/api/profiles/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
- package/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/delete/route.js +4 -3
- package/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/open-editor/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
- package/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
- package/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
- package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
- package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/sessions/[id]/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +5 -3
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +1 -0
- package/.next/server/chunks/[root-of-the-server]__005eb0c5._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__005eb0c5._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__09e90d57._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__09e90d57._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__18dd0ce9._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__18dd0ce9._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__19468536._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__19468536._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__1b87ec42._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1b87ec42._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__2b912935._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__2b912935._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__303d3bac._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__303d3bac._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__3fac2b91._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__3fac2b91._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__43440b8d._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__43440b8d._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__438f8bbe._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__438f8bbe._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__4a0bfb55._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__4a0bfb55._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__534c3949._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__534c3949._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__59160266._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__59160266._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__6f812da0._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__6f812da0._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__71aac504._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__71aac504._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__907a8bf2._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__907a8bf2._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__92089220._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__92089220._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__9b7bc2d0._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__9b7bc2d0._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__b2640944._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__b2640944._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__c2267cf1._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__c2267cf1._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__d7f7e6dd._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__6924c09d._.js.map → [root-of-the-server]__d7f7e6dd._.js.map} +1 -1
- package/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__d8e61048._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__f6d0d488._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__192ed2f4._.js.map → [root-of-the-server]__f6d0d488._.js.map} +1 -1
- package/.next/server/chunks/[root-of-the-server]__fa559e1e._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__fa559e1e._.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7e181e75.js +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7e181e75.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_fa835ac3.js +2 -2
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_fa835ac3.js.map +1 -1
- package/.next/server/chunks/src_lib_opencodeConfig_ts_8e209941._.js +3 -0
- package/.next/server/chunks/src_lib_opencodeConfig_ts_8e209941._.js.map +1 -0
- package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
- package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
- package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js.map → [root-of-the-server]__c91a8380._.js.map} +1 -1
- package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
- package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js.map +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +2 -2
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/routes-manifest.json +8 -0
- package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +2 -2
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/node/events/route.js +2 -2
- package/.next/standalone/.next/server/app/api/node/events/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route.js +2 -2
- package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js +2 -2
- package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/node/sessions/route.js +6 -4
- package/.next/standalone/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/nodes/route.js +2 -2
- package/.next/standalone/.next/server/app/api/nodes/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/opencode-config/route.js +2 -2
- package/.next/standalone/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/opencode-config/status/route.js +2 -2
- package/.next/standalone/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/opencode-events/route.js +3 -3
- package/.next/standalone/.next/server/app/api/opencode-events/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/[id]/apply/route.js +2 -2
- package/.next/standalone/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/[id]/export/route.js +2 -2
- package/.next/standalone/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/[id]/route.js +2 -2
- package/.next/standalone/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/import/route.js +2 -2
- package/.next/standalone/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/route.js +2 -2
- package/.next/standalone/.next/server/app/api/profiles/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
- package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js +4 -3
- package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js +2 -2
- package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/route.js +2 -2
- package/.next/standalone/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route.js +5 -3
- package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__005eb0c5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__09e90d57._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__18dd0ce9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19468536._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1b87ec42._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2b912935._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__303d3bac._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3fac2b91._.js +5 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__43440b8d._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__438f8bbe._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__4a0bfb55._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__534c3949._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__59160266._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6f812da0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__71aac504._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__907a8bf2._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__92089220._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__9b7bc2d0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b2640944._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__c2267cf1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d7f7e6dd._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f6d0d488._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__fa559e1e._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7e181e75.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_fa835ac3.js +2 -2
- package/.next/standalone/.next/server/chunks/src_lib_opencodeConfig_ts_8e209941._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +2 -2
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/9e790b67c80f853c.js +13 -0
- package/.next/standalone/.next/static/chunks/c3dc8cd80979c971.css +3 -0
- package/.next/standalone/AGENTS.md +4 -0
- package/.next/standalone/README.md +54 -5
- package/.next/standalone/check-hsql.mjs +1 -1
- package/.next/standalone/docs/session-status-detection.md +36 -0
- package/.next/standalone/docs/superpowers/specs/2026-04-09-claude-capability-alignment-design.md +39 -0
- package/.next/standalone/eslint.config.mjs +1 -0
- package/.next/standalone/package-lock.json +74 -13
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/src/app/api/node/events/route.ts +3 -3
- package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.test.ts +60 -1
- package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.ts +77 -22
- package/.next/standalone/src/app/api/node/sessions/[id]/delete/route.ts +6 -5
- package/.next/standalone/src/app/api/node/sessions/[id]/open-editor/route.ts +6 -5
- package/.next/standalone/src/app/api/node/sessions/route.test.ts +282 -0
- package/.next/standalone/src/app/api/node/sessions/route.ts +156 -30
- package/.next/standalone/src/app/api/opencode-config/route.test.ts +613 -0
- package/.next/standalone/src/app/api/opencode-config/route.ts +336 -185
- package/.next/standalone/src/app/api/opencode-events/route.test.ts +77 -1
- package/.next/standalone/src/app/api/opencode-events/route.ts +3 -3
- package/.next/standalone/src/app/api/opencode-models/route.test.ts +19 -0
- package/.next/standalone/src/app/api/opencode-models/route.ts +4 -1
- package/.next/standalone/src/app/api/profiles/[id]/apply/route.test.ts +227 -0
- package/.next/standalone/src/app/api/profiles/[id]/apply/route.ts +13 -9
- package/.next/standalone/src/app/api/sessions/[id]/archive/route.test.ts +126 -0
- package/.next/standalone/src/app/api/sessions/[id]/archive/route.ts +47 -12
- package/.next/standalone/src/app/api/sessions/[id]/delete/route.test.ts +140 -0
- package/.next/standalone/src/app/api/sessions/[id]/delete/route.ts +51 -16
- package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.test.ts +74 -0
- package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.ts +22 -2
- package/.next/standalone/src/app/api/sessions/[id]/restore/route.test.ts +186 -0
- package/.next/standalone/src/app/api/sessions/[id]/restore/route.ts +184 -0
- package/.next/standalone/src/app/api/sessions/[id]/route.ts +3 -3
- package/.next/standalone/src/app/api/sessions/route.test.ts +1955 -100
- package/.next/standalone/src/app/api/sessions/route.ts +361 -986
- package/.next/standalone/src/components/KanbanBoard.test.tsx +307 -1
- package/.next/standalone/src/components/KanbanBoard.tsx +106 -19
- package/.next/standalone/src/components/ProjectCard.test.tsx +420 -6
- package/.next/standalone/src/components/ProjectCard.tsx +238 -86
- package/.next/standalone/src/components/SessionCard.test.tsx +259 -8
- package/.next/standalone/src/components/SessionCard.tsx +182 -76
- package/.next/standalone/src/components/opencode-config/AgentConfigForm.test.tsx +141 -1
- package/.next/standalone/src/components/opencode-config/AgentConfigForm.tsx +99 -7
- package/.next/standalone/src/components/opencode-config/GeneralSettingsForm.test.tsx +11 -0
- package/.next/standalone/src/components/opencode-config/GeneralSettingsForm.tsx +41 -2
- package/.next/standalone/src/components/opencode-config/categories/CategoriesManager.tsx +3 -1
- package/.next/standalone/src/components/opencode-config/categories/CategoryConfigForm.test.tsx +106 -8
- package/.next/standalone/src/components/opencode-config/categories/CategoryConfigForm.tsx +82 -5
- package/.next/standalone/src/hooks/useHostSources.test.ts +0 -41
- package/.next/standalone/src/hooks/useOpencodeSync.test.ts +321 -1
- package/.next/standalone/src/hooks/useOpencodeSync.ts +16 -12
- package/.next/standalone/src/lib/claudeSessionOverrides.test.ts +75 -0
- package/.next/standalone/src/lib/claudeSessionOverrides.ts +169 -0
- package/.next/standalone/src/lib/fixtures/opencode-config/oh-my-openagent.v4.jsonc +70 -0
- package/.next/standalone/src/lib/fixtures/opencode-config/oh-my-openagent.v4.secret-like.jsonc +21 -0
- package/.next/standalone/src/lib/fixtures/opencode-config/oh-my-opencode.v3.jsonc +17 -0
- package/.next/standalone/src/lib/opencodeConfig.test.ts +430 -3
- package/.next/standalone/src/lib/opencodeConfig.ts +157 -4
- package/.next/standalone/src/lib/opencodeDiscovery.test.ts +241 -0
- package/.next/standalone/src/lib/opencodeDiscovery.ts +164 -9
- package/.next/standalone/src/lib/profiles/share.test.ts +92 -0
- package/.next/standalone/src/lib/profiles/share.ts +1 -0
- package/.next/standalone/src/lib/profiles/storage.test.ts +77 -1
- package/.next/standalone/src/lib/profiles/storage.ts +10 -9
- package/.next/standalone/src/lib/session-providers/claudeCode.test.ts +2288 -0
- package/.next/standalone/src/lib/session-providers/claudeCode.ts +1083 -0
- package/.next/standalone/src/lib/session-providers/localAggregator.test.ts +322 -0
- package/.next/standalone/src/lib/session-providers/localAggregator.ts +302 -0
- package/.next/standalone/src/lib/session-providers/opencodeProvider.test.ts +170 -0
- package/.next/standalone/src/lib/session-providers/opencodeProvider.ts +721 -0
- package/.next/standalone/src/lib/session-providers/opencodeSdkCompat.ts +92 -0
- package/.next/standalone/src/lib/session-providers/providerIds.test.ts +337 -0
- package/.next/standalone/src/lib/session-providers/providerIds.ts +176 -0
- package/.next/standalone/src/lib/session-providers/types.ts +131 -0
- package/.next/standalone/src/lib/transform.test.ts +253 -0
- package/.next/standalone/src/lib/transform.ts +96 -37
- package/.next/standalone/src/types/index.ts +23 -17
- package/.next/standalone/src/types/opencodeConfig.ts +55 -0
- package/.next/static/chunks/9e790b67c80f853c.js +13 -0
- package/.next/static/chunks/c3dc8cd80979c971.css +3 -0
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/.next/types/routes.d.ts +2 -1
- package/.next/types/validator.ts +9 -0
- package/README.md +54 -5
- package/package.json +2 -2
- package/.next/server/chunks/[root-of-the-server]__1211da38._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1211da38._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__192ed2f4._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__2b526e7a._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__2b526e7a._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__2f981540._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__3745b314._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__56690af0._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__56690af0._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__56f5f249._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__56f5f249._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__59175de4._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__59175de4._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__64fffc02._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__64fffc02._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__6924c09d._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__6c428a24._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__73a00b88._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__7e757f50._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__7e757f50._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__89c5eeab._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__89c5eeab._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__8da6c5a8._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__8da6c5a8._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__b796d06c._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__b796d06c._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__c2ce5c0f._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__c2ce5c0f._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__db285678._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__e00a9200._.js +0 -5
- package/.next/server/chunks/[root-of-the-server]__e00a9200._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__e5df5e5f._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__e5df5e5f._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__edbc8d9e._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__edbc8d9e._.js.map +0 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1211da38._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__192ed2f4._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2b526e7a._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__56690af0._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__56f5f249._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__59175de4._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__64fffc02._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6924c09d._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__7e757f50._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__89c5eeab._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__8da6c5a8._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b796d06c._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__c2ce5c0f._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__e00a9200._.js +0 -5
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__e5df5e5f._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__edbc8d9e._.js +0 -3
- package/.next/standalone/.next/static/chunks/65d5354ba0add961.js +0 -13
- package/.next/standalone/.next/static/chunks/f42202943f6742e5.css +0 -3
- package/.next/static/chunks/65d5354ba0add961.js +0 -13
- package/.next/static/chunks/f42202943f6742e5.css +0 -3
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → 0WaQ6UjiNBgvh531pJVh0}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → 0WaQ6UjiNBgvh531pJVh0}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → 0WaQ6UjiNBgvh531pJVh0}/_ssgManifest.js +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → 0WaQ6UjiNBgvh531pJVh0}/_buildManifest.js +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → 0WaQ6UjiNBgvh531pJVh0}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → 0WaQ6UjiNBgvh531pJVh0}/_ssgManifest.js +0 -0
|
@@ -1,152 +1,34 @@
|
|
|
1
|
-
import { createOpencodeClient } from '@opencode-ai/sdk';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
discoverOpencodePortsWithMeta,
|
|
6
|
-
discoverOpencodeProcessCwdsWithoutPortWithMeta,
|
|
7
|
-
} from '@/lib/opencodeDiscovery';
|
|
8
1
|
import { readConfig } from '@/lib/opencodeConfig';
|
|
2
|
+
import { claudeCodeLocalSessionProvider } from '@/lib/session-providers/claudeCode';
|
|
9
3
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
applyStickyBusyStatus,
|
|
5
|
+
applyStickyStatusStabilization,
|
|
6
|
+
getLocalSessionsResult,
|
|
7
|
+
shouldSkipSessionStatusStabilization,
|
|
8
|
+
} from '@/lib/session-providers/localAggregator';
|
|
9
|
+
import { opencodeLocalSessionProvider } from '@/lib/session-providers/opencodeProvider';
|
|
10
|
+
import type {
|
|
11
|
+
ChildEntry,
|
|
12
|
+
EnrichedSession,
|
|
13
|
+
HostAwareFields,
|
|
14
|
+
ProcessHint,
|
|
15
|
+
SessionHostStatus,
|
|
16
|
+
SessionsRouteResult,
|
|
17
|
+
SessionsSuccessPayload,
|
|
18
|
+
SessionSource,
|
|
19
|
+
SourceResultMeta,
|
|
20
|
+
} from '@/lib/session-providers/types';
|
|
21
|
+
|
|
22
|
+
import { parseSourceKey } from '@/lib/hostIdentity';
|
|
18
23
|
import { createNodeRequestHeaders, NODE_PROTOCOL_VERSION } from '@/lib/nodeProtocol';
|
|
24
|
+
import { composeProviderSourceKey, detectProviderFromRawId, extractProviderRawId } from '@/lib/session-providers/providerIds';
|
|
19
25
|
import { listNodeRecords, type StoredNodeRecord } from '@/lib/nodeRegistry';
|
|
20
26
|
import { RUNTIME_ROLE_ENV_VAR } from '@/lib/runtimeMode';
|
|
21
|
-
import type { BuiltInHostSource, RemoteHostConfig } from '@/types';
|
|
22
|
-
|
|
23
|
-
type SessionLike = {
|
|
24
|
-
id: string;
|
|
25
|
-
slug?: string;
|
|
26
|
-
title?: string;
|
|
27
|
-
directory: string;
|
|
28
|
-
debugReason?: string;
|
|
29
|
-
parentID?: string;
|
|
30
|
-
time?: {
|
|
31
|
-
created: number;
|
|
32
|
-
updated: number;
|
|
33
|
-
archived?: number;
|
|
34
|
-
};
|
|
35
|
-
};
|
|
27
|
+
import type { BuiltInHostSource, RemoteHostConfig, SessionCapabilities, SessionProvider } from '@/types';
|
|
36
28
|
|
|
37
|
-
const CHILD_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
38
|
-
const CHILD_UNKNOWN_STATE_BUSY_WINDOW_MS = 2 * 60 * 1000;
|
|
39
|
-
const CHILD_STATUS_MESSAGE_CHECK_LIMIT = 50;
|
|
40
|
-
const STALL_DETECTION_WINDOW_MS = 30 * 1000;
|
|
41
|
-
const STATUS_STICKY_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
42
|
-
const STATUS_STICKY_ABSENT_RETENTION_MS = 30 * 60 * 1000;
|
|
43
|
-
const DEFAULT_STATUS_STICKY_MAX_ENTRIES = 5000;
|
|
44
|
-
const GIT_COMMAND_TIMEOUT_MS = 1200;
|
|
45
|
-
const sessionListTimeoutMs = readPositiveTimeoutEnv('OPENCODE_SESSIONS_LIST_TIMEOUT_MS', 6000);
|
|
46
|
-
const sessionStatusTimeoutMs = readPositiveTimeoutEnv('OPENCODE_SESSIONS_STATUS_TIMEOUT_MS', 4000);
|
|
47
|
-
const sessionMessagesTimeoutMs = readPositiveTimeoutEnv('OPENCODE_SESSIONS_MESSAGES_TIMEOUT_MS', 2500);
|
|
48
29
|
const nodeSessionsTimeoutMs = readPositiveTimeoutEnv('VIBEPULSE_NODE_SESSIONS_TIMEOUT_MS', 6000);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
type StatusStickyState = {
|
|
53
|
-
lastBusyAt: number;
|
|
54
|
-
lastSeenAt: number;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const statusStickyState = new Map<string, StatusStickyState>();
|
|
58
|
-
|
|
59
|
-
function clearStickyStatusState(sessionId: string): void {
|
|
60
|
-
statusStickyState.delete(sessionId);
|
|
61
|
-
statusStickyState.delete(`child:${sessionId}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
type ChildEntry = HostAwareFields & {
|
|
65
|
-
id: string;
|
|
66
|
-
slug?: string;
|
|
67
|
-
title?: string;
|
|
68
|
-
directory?: string;
|
|
69
|
-
debugReason?: string;
|
|
70
|
-
parentID?: string;
|
|
71
|
-
time?: { created: number; updated: number; archived?: number };
|
|
72
|
-
realTimeStatus: string;
|
|
73
|
-
waitingForUser: boolean;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
type EnrichedSession = SessionLike & HostAwareFields & {
|
|
77
|
-
projectName: string;
|
|
78
|
-
branch: string | null;
|
|
79
|
-
realTimeStatus: 'idle' | 'busy' | 'retry';
|
|
80
|
-
waitingForUser: boolean;
|
|
81
|
-
children: ChildEntry[];
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
type SessionStatusStabilizationTarget = {
|
|
85
|
-
id: string;
|
|
86
|
-
time?: {
|
|
87
|
-
archived?: number;
|
|
88
|
-
};
|
|
89
|
-
realTimeStatus: string;
|
|
90
|
-
waitingForUser: boolean;
|
|
91
|
-
children: Array<{
|
|
92
|
-
id: string;
|
|
93
|
-
time?: {
|
|
94
|
-
archived?: number;
|
|
95
|
-
};
|
|
96
|
-
realTimeStatus: string;
|
|
97
|
-
waitingForUser: boolean;
|
|
98
|
-
}>;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
type ProcessHint = {
|
|
102
|
-
pid: number;
|
|
103
|
-
directory: string;
|
|
104
|
-
projectName: string;
|
|
105
|
-
reason: 'process_without_api_port';
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
type SessionSource = BuiltInHostSource | (RemoteHostConfig & { hostKind: 'remote' });
|
|
109
|
-
|
|
110
|
-
type HostAwareFields = {
|
|
111
|
-
hostId?: string;
|
|
112
|
-
hostLabel?: string;
|
|
113
|
-
hostKind?: SessionSource['hostKind'];
|
|
114
|
-
hostBaseUrl?: string;
|
|
115
|
-
rawSessionId?: string;
|
|
116
|
-
sourceSessionKey?: string;
|
|
117
|
-
readOnly?: boolean;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
type SessionHostStatus = {
|
|
121
|
-
hostId: string;
|
|
122
|
-
hostLabel: string;
|
|
123
|
-
hostKind: SessionSource['hostKind'];
|
|
124
|
-
online: boolean;
|
|
125
|
-
degraded?: boolean;
|
|
126
|
-
reason?: string;
|
|
127
|
-
baseUrl?: string;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
type SourceResultMeta = {
|
|
131
|
-
online: boolean;
|
|
132
|
-
degraded?: boolean;
|
|
133
|
-
reason?: string;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
type SessionsSuccessPayload = {
|
|
137
|
-
sessions: EnrichedSession[];
|
|
138
|
-
processHints: ProcessHint[];
|
|
139
|
-
failedPorts?: Array<{ port: number; reason: string }>;
|
|
140
|
-
degraded?: boolean;
|
|
141
|
-
hosts?: SessionHostStatus[];
|
|
142
|
-
hostStatuses?: SessionHostStatus[];
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
type SessionsRouteResult = {
|
|
146
|
-
payload: SessionsSuccessPayload | Record<string, unknown>;
|
|
147
|
-
status?: number;
|
|
148
|
-
sourceMeta?: SourceResultMeta;
|
|
149
|
-
};
|
|
30
|
+
const CLAUDE_INFERRED_PARENT_MAX_CREATED_GAP_MS = 60_000;
|
|
31
|
+
const CLAUDE_INFERRED_PARENT_AMBIGUITY_GAP_MS = 5_000;
|
|
150
32
|
|
|
151
33
|
const LOCAL_SOURCE: BuiltInHostSource = {
|
|
152
34
|
hostId: 'local',
|
|
@@ -154,8 +36,16 @@ const LOCAL_SOURCE: BuiltInHostSource = {
|
|
|
154
36
|
hostKind: 'local',
|
|
155
37
|
};
|
|
156
38
|
|
|
39
|
+
const LOCAL_POLLING_PROVIDERS = [opencodeLocalSessionProvider, claudeCodeLocalSessionProvider] as const;
|
|
40
|
+
|
|
157
41
|
export const dynamic = 'force-dynamic';
|
|
158
42
|
|
|
43
|
+
export {
|
|
44
|
+
applyStickyBusyStatus,
|
|
45
|
+
applyStickyStatusStabilization,
|
|
46
|
+
shouldSkipSessionStatusStabilization,
|
|
47
|
+
};
|
|
48
|
+
|
|
159
49
|
export async function GET() {
|
|
160
50
|
return handleGet();
|
|
161
51
|
}
|
|
@@ -164,14 +54,6 @@ export async function POST(request: Request) {
|
|
|
164
54
|
return handlePost(request);
|
|
165
55
|
}
|
|
166
56
|
|
|
167
|
-
type MessageStateStatus = string;
|
|
168
|
-
|
|
169
|
-
type MessagePart = {
|
|
170
|
-
state?: {
|
|
171
|
-
status?: unknown;
|
|
172
|
-
};
|
|
173
|
-
};
|
|
174
|
-
|
|
175
57
|
function readPositiveTimeoutEnv(name: string, fallback: number): number {
|
|
176
58
|
const raw = process.env[name];
|
|
177
59
|
const parsed = Number(raw);
|
|
@@ -181,300 +63,6 @@ function readPositiveTimeoutEnv(name: string, fallback: number): number {
|
|
|
181
63
|
return fallback;
|
|
182
64
|
}
|
|
183
65
|
|
|
184
|
-
function withTimeout<T>(operation: (signal: AbortSignal) => Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
185
|
-
const timeoutError = new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
186
|
-
const timeoutController = new AbortController();
|
|
187
|
-
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
188
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
189
|
-
timeoutHandle = setTimeout(() => {
|
|
190
|
-
timeoutController.abort();
|
|
191
|
-
reject(timeoutError);
|
|
192
|
-
}, timeoutMs);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const operationPromise = operation(timeoutController.signal).catch((error) => {
|
|
196
|
-
if (timeoutController.signal.aborted) {
|
|
197
|
-
throw timeoutError;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
throw error;
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
return Promise.race([operationPromise, timeoutPromise]).finally(() => {
|
|
204
|
-
if (timeoutHandle) {
|
|
205
|
-
clearTimeout(timeoutHandle);
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const WAITING_PART_STATUSES = new Set<string>([
|
|
211
|
-
'awaiting-input',
|
|
212
|
-
'awaiting_input',
|
|
213
|
-
'input-required',
|
|
214
|
-
'input_required',
|
|
215
|
-
'requires-input',
|
|
216
|
-
'requires_input',
|
|
217
|
-
'blocked',
|
|
218
|
-
'paused',
|
|
219
|
-
]);
|
|
220
|
-
|
|
221
|
-
function normalizePartStatus(status: string): string {
|
|
222
|
-
return status.trim().toLowerCase();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function isWaitingPartStatus(status: string): boolean {
|
|
226
|
-
return WAITING_PART_STATUSES.has(normalizePartStatus(status));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function collectPartStatuses(messages: Array<{ parts?: MessagePart[] }>): MessageStateStatus[] {
|
|
230
|
-
const partStatuses: MessageStateStatus[] = [];
|
|
231
|
-
|
|
232
|
-
for (const message of messages) {
|
|
233
|
-
for (const part of message.parts || []) {
|
|
234
|
-
const status = part?.state?.status;
|
|
235
|
-
if (typeof status === 'string') {
|
|
236
|
-
const normalized = normalizePartStatus(status);
|
|
237
|
-
if (normalized) {
|
|
238
|
-
partStatuses.push(normalized);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return partStatuses;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function fetchPartStatuses(
|
|
248
|
-
client: ReturnType<typeof createOpencodeClient>,
|
|
249
|
-
sessionId: string,
|
|
250
|
-
timeoutMs: number
|
|
251
|
-
): Promise<MessageStateStatus[]> {
|
|
252
|
-
const messagesResult = await withTimeout(
|
|
253
|
-
(signal) =>
|
|
254
|
-
client.session.messages({
|
|
255
|
-
path: { id: sessionId },
|
|
256
|
-
query: { limit: 8 },
|
|
257
|
-
signal,
|
|
258
|
-
}),
|
|
259
|
-
timeoutMs,
|
|
260
|
-
`session.messages(${sessionId})`
|
|
261
|
-
);
|
|
262
|
-
const messages = (messagesResult.data || []) as Array<{ parts?: MessagePart[] }>;
|
|
263
|
-
return collectPartStatuses(messages);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function getUpdatedAt(session: { time?: { updated?: number; created?: number } }): number {
|
|
267
|
-
return session.time?.updated || session.time?.created || 0;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function normalizeRealtimeStatus(value: string | undefined): StableRealtimeStatus {
|
|
271
|
-
if (value === 'busy' || value === 'retry') return value;
|
|
272
|
-
return 'idle';
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function applyStickyBusyStatus(id: string, status: StableRealtimeStatus, now: number, stickyBusyWindowMs: number): StableRealtimeStatus {
|
|
276
|
-
const existing = statusStickyState.get(id) ?? { lastBusyAt: 0, lastSeenAt: now };
|
|
277
|
-
|
|
278
|
-
if (status === 'busy') {
|
|
279
|
-
existing.lastBusyAt = now;
|
|
280
|
-
existing.lastSeenAt = now;
|
|
281
|
-
statusStickyState.set(id, existing);
|
|
282
|
-
return status;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (status === 'retry') {
|
|
286
|
-
existing.lastSeenAt = now;
|
|
287
|
-
statusStickyState.set(id, existing);
|
|
288
|
-
return status;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const shouldKeepBusy = existing.lastBusyAt > 0 && now - existing.lastBusyAt <= stickyBusyWindowMs;
|
|
292
|
-
existing.lastSeenAt = now;
|
|
293
|
-
statusStickyState.set(id, existing);
|
|
294
|
-
return shouldKeepBusy ? 'busy' : 'idle';
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function getStickyStateMaxEntries(): number {
|
|
298
|
-
const raw = Number(process.env.OPENCODE_STATUS_STICKY_MAX_ENTRIES);
|
|
299
|
-
if (Number.isFinite(raw) && raw > 0) {
|
|
300
|
-
return Math.floor(raw);
|
|
301
|
-
}
|
|
302
|
-
return DEFAULT_STATUS_STICKY_MAX_ENTRIES;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function pruneStickyState(now: number, activeIds: Set<string>): void {
|
|
306
|
-
for (const [id, state] of statusStickyState) {
|
|
307
|
-
const ageMs = now - state.lastSeenAt;
|
|
308
|
-
const isActive = activeIds.has(id);
|
|
309
|
-
if (ageMs > STATUS_STICKY_RETENTION_MS || (!isActive && ageMs > STATUS_STICKY_ABSENT_RETENTION_MS)) {
|
|
310
|
-
statusStickyState.delete(id);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const maxEntries = getStickyStateMaxEntries();
|
|
315
|
-
if (statusStickyState.size <= maxEntries) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const overflow = statusStickyState.size - maxEntries;
|
|
320
|
-
const sortedByLastSeen = Array.from(statusStickyState.entries()).sort((a, b) => a[1].lastSeenAt - b[1].lastSeenAt);
|
|
321
|
-
|
|
322
|
-
let removed = 0;
|
|
323
|
-
for (const [id] of sortedByLastSeen) {
|
|
324
|
-
if (removed >= overflow) break;
|
|
325
|
-
if (activeIds.has(id)) continue;
|
|
326
|
-
statusStickyState.delete(id);
|
|
327
|
-
removed++;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (removed >= overflow) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
for (const [id] of sortedByLastSeen) {
|
|
335
|
-
if (removed >= overflow) break;
|
|
336
|
-
if (!statusStickyState.has(id)) continue;
|
|
337
|
-
statusStickyState.delete(id);
|
|
338
|
-
removed++;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function hasRecentActivity(session: { time?: { updated?: number } }, now: number): boolean {
|
|
343
|
-
const updatedAt = session.time?.updated;
|
|
344
|
-
if (!updatedAt) return false;
|
|
345
|
-
return now - updatedAt <= STALL_DETECTION_WINDOW_MS;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function toChildEntry(
|
|
349
|
-
child: SessionLike,
|
|
350
|
-
status: 'idle' | 'busy' | 'retry',
|
|
351
|
-
waitingForUser = false
|
|
352
|
-
): ChildEntry {
|
|
353
|
-
return {
|
|
354
|
-
id: child.id,
|
|
355
|
-
slug: child.slug,
|
|
356
|
-
title: child.title,
|
|
357
|
-
directory: child.directory,
|
|
358
|
-
debugReason: child.debugReason,
|
|
359
|
-
parentID: child.parentID,
|
|
360
|
-
time: child.time,
|
|
361
|
-
realTimeStatus: status,
|
|
362
|
-
waitingForUser,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function clearSessionStabilizationState(session: SessionStatusStabilizationTarget): void {
|
|
367
|
-
clearStickyStatusState(session.id);
|
|
368
|
-
clearSessionForceUnarchived(session.id);
|
|
369
|
-
for (const child of session.children) {
|
|
370
|
-
clearStickyStatusState(`child:${child.id}`);
|
|
371
|
-
clearSessionForceUnarchived(child.id);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function shouldSkipSessionStatusStabilization(
|
|
376
|
-
session: SessionStatusStabilizationTarget,
|
|
377
|
-
now: number
|
|
378
|
-
): boolean {
|
|
379
|
-
if (takeSessionStickyStatusBlocked(session.id, now)) {
|
|
380
|
-
clearSessionStabilizationState(session);
|
|
381
|
-
return true;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (session.time?.archived) {
|
|
385
|
-
clearSessionStabilizationState(session);
|
|
386
|
-
return true;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export function applyStickyStatusStabilization(
|
|
393
|
-
session: SessionStatusStabilizationTarget,
|
|
394
|
-
stickyNow: number,
|
|
395
|
-
stickyBusyDelayMs: number
|
|
396
|
-
): void {
|
|
397
|
-
for (const child of session.children) {
|
|
398
|
-
if (child.time?.archived) {
|
|
399
|
-
clearStickyStatusState(`child:${child.id}`);
|
|
400
|
-
clearSessionForceUnarchived(child.id);
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const normalizedChildStatus = normalizeRealtimeStatus(child.realTimeStatus);
|
|
405
|
-
const childStatusForStabilization =
|
|
406
|
-
child.waitingForUser && normalizedChildStatus === 'idle' ? 'retry' : normalizedChildStatus;
|
|
407
|
-
child.realTimeStatus = applyStickyBusyStatus(
|
|
408
|
-
`child:${child.id}`,
|
|
409
|
-
childStatusForStabilization,
|
|
410
|
-
stickyNow,
|
|
411
|
-
stickyBusyDelayMs
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
if (child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry' || child.waitingForUser) {
|
|
415
|
-
markSessionForceUnarchived(child.id, stickyNow);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const normalizedSessionStatus = normalizeRealtimeStatus(session.realTimeStatus);
|
|
420
|
-
const sessionStatusForStabilization =
|
|
421
|
-
session.waitingForUser && normalizedSessionStatus === 'idle' ? 'retry' : normalizedSessionStatus;
|
|
422
|
-
session.realTimeStatus = applyStickyBusyStatus(
|
|
423
|
-
session.id,
|
|
424
|
-
sessionStatusForStabilization,
|
|
425
|
-
stickyNow,
|
|
426
|
-
stickyBusyDelayMs
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
const hasActiveChildren = session.children.some(
|
|
430
|
-
(child) => child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry' || child.waitingForUser
|
|
431
|
-
);
|
|
432
|
-
const shouldAutoUnarchive =
|
|
433
|
-
session.realTimeStatus === 'busy' ||
|
|
434
|
-
session.realTimeStatus === 'retry' ||
|
|
435
|
-
session.waitingForUser ||
|
|
436
|
-
hasActiveChildren;
|
|
437
|
-
|
|
438
|
-
if (shouldAutoUnarchive) {
|
|
439
|
-
markSessionForceUnarchived(session.id, stickyNow);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
// Get project name from directory path
|
|
443
|
-
function getProjectName(directory: string): string {
|
|
444
|
-
return path.basename(directory);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Check if directory is a git repository
|
|
448
|
-
function isGitRepo(directory: string): boolean {
|
|
449
|
-
try {
|
|
450
|
-
const result = execSync('git rev-parse --is-inside-work-tree', {
|
|
451
|
-
cwd: directory,
|
|
452
|
-
encoding: 'utf-8',
|
|
453
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
454
|
-
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
455
|
-
});
|
|
456
|
-
return result.trim() === 'true';
|
|
457
|
-
} catch {
|
|
458
|
-
return false;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Get git branch name
|
|
463
|
-
function getGitBranch(directory: string): string | null {
|
|
464
|
-
if (!isGitRepo(directory)) return null;
|
|
465
|
-
try {
|
|
466
|
-
const branch = execSync('git branch --show-current', {
|
|
467
|
-
cwd: directory,
|
|
468
|
-
encoding: 'utf-8',
|
|
469
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
470
|
-
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
471
|
-
});
|
|
472
|
-
return branch.trim() || null;
|
|
473
|
-
} catch {
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
66
|
async function readStickyBusyDelayMs(): Promise<number> {
|
|
479
67
|
let stickyBusyDelayMs = 1000; // default 1s
|
|
480
68
|
try {
|
|
@@ -502,6 +90,23 @@ function isRemoteSource(source: SessionSource): source is RemoteHostConfig & { h
|
|
|
502
90
|
return source.hostKind === 'remote';
|
|
503
91
|
}
|
|
504
92
|
|
|
93
|
+
function getRemoteClaudeCapabilities(
|
|
94
|
+
capabilities: SessionCapabilities | undefined,
|
|
95
|
+
provider: SessionProvider,
|
|
96
|
+
source: SessionSource
|
|
97
|
+
): SessionCapabilities | undefined {
|
|
98
|
+
if (!isRemoteSource(source) || provider !== 'claude-code') {
|
|
99
|
+
return capabilities;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
openProject: capabilities?.openProject ?? true,
|
|
104
|
+
openEditor: false,
|
|
105
|
+
archive: false,
|
|
106
|
+
delete: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
505
110
|
function normalizeNodeBaseUrl(baseUrl: string): string | null {
|
|
506
111
|
const trimmed = baseUrl.trim();
|
|
507
112
|
if (!trimmed) {
|
|
@@ -551,10 +156,26 @@ function isTimeValue(value: unknown): boolean {
|
|
|
551
156
|
return (
|
|
552
157
|
typeof created === 'number' &&
|
|
553
158
|
typeof updated === 'number' &&
|
|
554
|
-
(archived === undefined || typeof archived === 'number')
|
|
159
|
+
(archived === undefined || archived === null || typeof archived === 'number')
|
|
555
160
|
);
|
|
556
161
|
}
|
|
557
162
|
|
|
163
|
+
function normalizeSessionTimeValue<T extends { created: number; updated: number; archived?: number } | undefined>(time: T): T {
|
|
164
|
+
if (!time) {
|
|
165
|
+
return time;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const archived = (time as { archived?: number | null }).archived;
|
|
169
|
+
if (archived === null) {
|
|
170
|
+
return {
|
|
171
|
+
created: time.created,
|
|
172
|
+
updated: time.updated,
|
|
173
|
+
} as T;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return time;
|
|
177
|
+
}
|
|
178
|
+
|
|
558
179
|
function isChildEntryValue(value: unknown): value is ChildEntry {
|
|
559
180
|
if (!isRecord(value)) {
|
|
560
181
|
return false;
|
|
@@ -784,25 +405,62 @@ function toRawSessionId(value: string): string {
|
|
|
784
405
|
}
|
|
785
406
|
}
|
|
786
407
|
|
|
787
|
-
function
|
|
408
|
+
function toProviderRawSessionId(value: string): string {
|
|
409
|
+
return extractProviderRawId(toRawSessionId(value));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function composeProviderSourceKeySafely(
|
|
413
|
+
hostId: string,
|
|
414
|
+
rawId: string,
|
|
415
|
+
readOnly?: boolean,
|
|
416
|
+
provider?: SessionProvider
|
|
417
|
+
): string | undefined {
|
|
788
418
|
try {
|
|
789
|
-
return
|
|
419
|
+
return composeProviderSourceKey(hostId, rawId, {
|
|
420
|
+
readOnly,
|
|
421
|
+
...(provider ? { provider } : {}),
|
|
422
|
+
}).sourceKey;
|
|
790
423
|
} catch {
|
|
791
424
|
return undefined;
|
|
792
425
|
}
|
|
793
426
|
}
|
|
794
427
|
|
|
795
|
-
function addHostMetadataToChildEntry(
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
428
|
+
function addHostMetadataToChildEntry(
|
|
429
|
+
child: ChildEntry,
|
|
430
|
+
source: SessionSource,
|
|
431
|
+
parentSourceSessionKey?: string
|
|
432
|
+
): ChildEntry | null {
|
|
433
|
+
const rawSessionId = child.rawSessionId ?? toProviderRawSessionId(child.id);
|
|
434
|
+
const rawParentId = child.parentID ? toProviderRawSessionId(child.parentID) : child.parentID;
|
|
435
|
+
const inferredProvider = detectProviderFromRawId(child.id);
|
|
436
|
+
const parentProvider = parentSourceSessionKey ? detectProviderFromRawId(parentSourceSessionKey) : undefined;
|
|
437
|
+
const childProvider = inferredProvider === 'claude-code' ? inferredProvider : (parentProvider ?? inferredProvider);
|
|
438
|
+
const childCapabilities = getRemoteClaudeCapabilities(child.capabilities, childProvider, source);
|
|
439
|
+
const sourceSessionKey = composeProviderSourceKeySafely(source.hostId, rawSessionId, child.readOnly, childProvider);
|
|
799
440
|
if (!sourceSessionKey) {
|
|
800
441
|
return null;
|
|
801
442
|
}
|
|
802
443
|
|
|
444
|
+
const parentSourceRawId = parentSourceSessionKey
|
|
445
|
+
? toProviderRawSessionId(parentSourceSessionKey)
|
|
446
|
+
: undefined;
|
|
447
|
+
const shouldReuseParentSourceKey =
|
|
448
|
+
typeof rawParentId === 'string'
|
|
449
|
+
&& typeof parentSourceSessionKey === 'string'
|
|
450
|
+
&& parentSourceRawId === rawParentId;
|
|
451
|
+
|
|
803
452
|
const sourceParentKey = rawParentId
|
|
804
|
-
? (
|
|
453
|
+
? (
|
|
454
|
+
shouldReuseParentSourceKey
|
|
455
|
+
? parentSourceSessionKey
|
|
456
|
+
: composeProviderSourceKeySafely(source.hostId, rawParentId, undefined, childProvider) ?? undefined
|
|
457
|
+
)
|
|
805
458
|
: undefined;
|
|
459
|
+
const normalizedChildProvider = child.provider ?? (childProvider === 'claude-code' ? childProvider : undefined);
|
|
460
|
+
const normalizedChildProviderRawId =
|
|
461
|
+
normalizedChildProvider === 'claude-code'
|
|
462
|
+
? (child.providerRawId ?? rawSessionId)
|
|
463
|
+
: child.providerRawId;
|
|
806
464
|
|
|
807
465
|
return {
|
|
808
466
|
...child,
|
|
@@ -812,26 +470,37 @@ function addHostMetadataToChildEntry(child: ChildEntry, source: SessionSource):
|
|
|
812
470
|
hostLabel: source.hostLabel,
|
|
813
471
|
hostKind: source.hostKind,
|
|
814
472
|
...(isRemoteSource(source) ? { hostBaseUrl: source.baseUrl } : {}),
|
|
473
|
+
time: normalizeSessionTimeValue(child.time),
|
|
815
474
|
rawSessionId,
|
|
816
475
|
sourceSessionKey,
|
|
817
|
-
readOnly: false,
|
|
476
|
+
readOnly: child.readOnly ?? false,
|
|
477
|
+
capabilities: childCapabilities,
|
|
478
|
+
...(normalizedChildProvider ? { provider: normalizedChildProvider } : {}),
|
|
479
|
+
...(normalizedChildProviderRawId ? { providerRawId: normalizedChildProviderRawId } : {}),
|
|
818
480
|
};
|
|
819
481
|
}
|
|
820
482
|
|
|
821
483
|
function addHostMetadataToSession(session: EnrichedSession, source: SessionSource): EnrichedSession | null {
|
|
822
|
-
const rawSessionId = session.rawSessionId ??
|
|
823
|
-
const rawParentId = session.parentID ?
|
|
824
|
-
const
|
|
484
|
+
const rawSessionId = session.rawSessionId ?? toProviderRawSessionId(session.id);
|
|
485
|
+
const rawParentId = session.parentID ? toProviderRawSessionId(session.parentID) : session.parentID;
|
|
486
|
+
const sessionProvider = detectProviderFromRawId(session.id);
|
|
487
|
+
const sessionCapabilities = getRemoteClaudeCapabilities(session.capabilities, sessionProvider, source);
|
|
488
|
+
const sourceSessionKey = composeProviderSourceKeySafely(source.hostId, rawSessionId, session.readOnly, sessionProvider);
|
|
825
489
|
if (!sourceSessionKey) {
|
|
826
490
|
return null;
|
|
827
491
|
}
|
|
828
492
|
|
|
829
493
|
const sourceParentKey = rawParentId
|
|
830
|
-
? (
|
|
494
|
+
? (composeProviderSourceKeySafely(source.hostId, rawParentId, undefined, sessionProvider) ?? undefined)
|
|
831
495
|
: undefined;
|
|
496
|
+
const normalizedSessionProvider = session.provider ?? (sessionProvider === 'claude-code' ? sessionProvider : undefined);
|
|
497
|
+
const normalizedSessionProviderRawId =
|
|
498
|
+
normalizedSessionProvider === 'claude-code'
|
|
499
|
+
? (session.providerRawId ?? rawSessionId)
|
|
500
|
+
: session.providerRawId;
|
|
832
501
|
const children: ChildEntry[] = [];
|
|
833
502
|
for (const child of session.children) {
|
|
834
|
-
const enrichedChild = addHostMetadataToChildEntry(child, source);
|
|
503
|
+
const enrichedChild = addHostMetadataToChildEntry(child, source, sourceSessionKey);
|
|
835
504
|
if (enrichedChild) {
|
|
836
505
|
children.push(enrichedChild);
|
|
837
506
|
}
|
|
@@ -845,9 +514,13 @@ function addHostMetadataToSession(session: EnrichedSession, source: SessionSourc
|
|
|
845
514
|
hostLabel: source.hostLabel,
|
|
846
515
|
hostKind: source.hostKind,
|
|
847
516
|
...(isRemoteSource(source) ? { hostBaseUrl: source.baseUrl } : {}),
|
|
517
|
+
time: normalizeSessionTimeValue(session.time),
|
|
848
518
|
rawSessionId,
|
|
849
519
|
sourceSessionKey,
|
|
850
|
-
readOnly: false,
|
|
520
|
+
readOnly: session.readOnly ?? false,
|
|
521
|
+
capabilities: sessionCapabilities,
|
|
522
|
+
...(normalizedSessionProvider ? { provider: normalizedSessionProvider } : {}),
|
|
523
|
+
...(normalizedSessionProviderRawId ? { providerRawId: normalizedSessionProviderRawId } : {}),
|
|
851
524
|
children,
|
|
852
525
|
};
|
|
853
526
|
}
|
|
@@ -873,11 +546,235 @@ function addHostMetadataToPayload(payload: Record<string, unknown>, source: Sess
|
|
|
873
546
|
|
|
874
547
|
return {
|
|
875
548
|
...payload,
|
|
876
|
-
sessions,
|
|
549
|
+
sessions: rebuildAggregateClaudeTopology(sessions),
|
|
877
550
|
...(payloadDegraded || droppedSessions > 0 ? { degraded: true } : {}),
|
|
878
551
|
};
|
|
879
552
|
}
|
|
880
553
|
|
|
554
|
+
function toAggregateClaudeChildEntry(session: EnrichedSession): ChildEntry {
|
|
555
|
+
const childEntry = { ...session };
|
|
556
|
+
delete (childEntry as { children?: unknown }).children;
|
|
557
|
+
return childEntry as ChildEntry;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function hasClaudeProvider(entry: HostAwareFields): boolean {
|
|
561
|
+
return entry.provider === 'claude-code';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function toFiniteTimestamp(value: unknown): number | undefined {
|
|
565
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function getCreatedAt(session: EnrichedSession): number | undefined {
|
|
569
|
+
return toFiniteTimestamp(session.time?.created);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function getUpdatedAt(session: EnrichedSession): number | undefined {
|
|
573
|
+
return toFiniteTimestamp(session.time?.updated);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function hasSameHostIdentity(a: HostAwareFields, b: HostAwareFields): boolean {
|
|
577
|
+
return a.hostId === b.hostId && a.hostKind === b.hostKind;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
type InferredClaudeParentCandidate = {
|
|
581
|
+
parent: EnrichedSession;
|
|
582
|
+
createdGapMs: number;
|
|
583
|
+
updatedGapMs: number;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
function inferClaudeParentSession(
|
|
587
|
+
child: EnrichedSession,
|
|
588
|
+
sessions: EnrichedSession[],
|
|
589
|
+
absorbedChildIds: Set<string>
|
|
590
|
+
): EnrichedSession | undefined {
|
|
591
|
+
if (child.topology?.childSessions === 'authoritative') {
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const childCreatedAt = getCreatedAt(child);
|
|
596
|
+
if (childCreatedAt === undefined) {
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const childUpdatedAt = getUpdatedAt(child) ?? childCreatedAt;
|
|
601
|
+
const candidates: InferredClaudeParentCandidate[] = [];
|
|
602
|
+
|
|
603
|
+
for (const parent of sessions) {
|
|
604
|
+
if (parent.id === child.id || absorbedChildIds.has(parent.id)) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!hasClaudeProvider(parent) || !hasSameHostIdentity(parent, child)) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (typeof parent.parentID === 'string') {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (parent.directory !== child.directory || parent.projectName !== child.projectName) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const parentCreatedAt = getCreatedAt(parent);
|
|
621
|
+
if (parentCreatedAt === undefined || parentCreatedAt > childCreatedAt) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const createdGapMs = childCreatedAt - parentCreatedAt;
|
|
626
|
+
if (createdGapMs > CLAUDE_INFERRED_PARENT_MAX_CREATED_GAP_MS) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const parentLooksActive =
|
|
631
|
+
parent.realTimeStatus === 'busy' ||
|
|
632
|
+
parent.realTimeStatus === 'retry' ||
|
|
633
|
+
parent.waitingForUser;
|
|
634
|
+
if (!parentLooksActive) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const parentUpdatedAt = getUpdatedAt(parent) ?? parentCreatedAt;
|
|
639
|
+
const updatedGapMs = Math.abs(childUpdatedAt - parentUpdatedAt);
|
|
640
|
+
|
|
641
|
+
candidates.push({
|
|
642
|
+
parent,
|
|
643
|
+
createdGapMs,
|
|
644
|
+
updatedGapMs,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (candidates.length === 0) {
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
candidates.sort((a, b) => {
|
|
653
|
+
if (a.createdGapMs !== b.createdGapMs) {
|
|
654
|
+
return a.createdGapMs - b.createdGapMs;
|
|
655
|
+
}
|
|
656
|
+
return a.updatedGapMs - b.updatedGapMs;
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const bestCandidate = candidates[0];
|
|
660
|
+
const secondCandidate = candidates[1];
|
|
661
|
+
|
|
662
|
+
if (
|
|
663
|
+
secondCandidate
|
|
664
|
+
&& Math.abs(secondCandidate.createdGapMs - bestCandidate.createdGapMs) <= CLAUDE_INFERRED_PARENT_AMBIGUITY_GAP_MS
|
|
665
|
+
) {
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return bestCandidate.parent;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function rebuildAggregateClaudeTopology(sessions: EnrichedSession[]): EnrichedSession[] {
|
|
673
|
+
const sessionsById = new Map(sessions.map((session) => [session.id, session]));
|
|
674
|
+
const absorbedChildIds = new Set<string>();
|
|
675
|
+
const resolveVisibleClaudeParent = (
|
|
676
|
+
childSession: EnrichedSession,
|
|
677
|
+
parentSession: EnrichedSession
|
|
678
|
+
): EnrichedSession | undefined => {
|
|
679
|
+
let cursor: EnrichedSession | undefined = parentSession;
|
|
680
|
+
const visited = new Set<string>([childSession.id]);
|
|
681
|
+
|
|
682
|
+
while (cursor) {
|
|
683
|
+
if (visited.has(cursor.id)) {
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
visited.add(cursor.id);
|
|
688
|
+
|
|
689
|
+
if (!absorbedChildIds.has(cursor.id)) {
|
|
690
|
+
return cursor;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (typeof cursor.parentID !== 'string') {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
cursor = sessionsById.get(cursor.parentID);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return undefined;
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const orderedSessions = [...sessions].sort((a, b) => {
|
|
704
|
+
const createdDiff = (getCreatedAt(b) ?? 0) - (getCreatedAt(a) ?? 0);
|
|
705
|
+
if (createdDiff !== 0) {
|
|
706
|
+
return createdDiff;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return (getUpdatedAt(b) ?? 0) - (getUpdatedAt(a) ?? 0);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
for (const session of orderedSessions) {
|
|
713
|
+
if (!hasClaudeProvider(session) || absorbedChildIds.has(session.id)) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let parentSession: EnrichedSession | undefined;
|
|
718
|
+
|
|
719
|
+
if (typeof session.parentID === 'string') {
|
|
720
|
+
const explicitParentSession = sessionsById.get(session.parentID);
|
|
721
|
+
if (explicitParentSession) {
|
|
722
|
+
if (
|
|
723
|
+
explicitParentSession === session
|
|
724
|
+
|| !hasClaudeProvider(explicitParentSession)
|
|
725
|
+
|| !hasSameHostIdentity(explicitParentSession, session)
|
|
726
|
+
) {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const visibleParentSession = resolveVisibleClaudeParent(session, explicitParentSession);
|
|
731
|
+
if (!visibleParentSession) {
|
|
732
|
+
if (absorbedChildIds.has(explicitParentSession.id)) {
|
|
733
|
+
session.parentID = undefined;
|
|
734
|
+
}
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
parentSession = visibleParentSession;
|
|
739
|
+
if (session.parentID !== visibleParentSession.id) {
|
|
740
|
+
session.parentID = visibleParentSession.id;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (!parentSession) {
|
|
746
|
+
parentSession = inferClaudeParentSession(session, sessions, absorbedChildIds);
|
|
747
|
+
if (parentSession) {
|
|
748
|
+
session.parentID = parentSession.id;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (!parentSession) {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (session.children.length > 0) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const childEntry = toAggregateClaudeChildEntry(session);
|
|
761
|
+
const existingChildIndex = parentSession.children.findIndex((child) => child.id === childEntry.id);
|
|
762
|
+
if (existingChildIndex >= 0) {
|
|
763
|
+
parentSession.children[existingChildIndex] = {
|
|
764
|
+
...parentSession.children[existingChildIndex],
|
|
765
|
+
...childEntry,
|
|
766
|
+
};
|
|
767
|
+
} else {
|
|
768
|
+
parentSession.children.push(childEntry);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
sortChildEntries(parentSession.children);
|
|
772
|
+
absorbedChildIds.add(session.id);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return sessions.filter((session) => !absorbedChildIds.has(session.id));
|
|
776
|
+
}
|
|
777
|
+
|
|
881
778
|
function sortChildEntries(children: ChildEntry[]): void {
|
|
882
779
|
children.sort((a, b) => {
|
|
883
780
|
const aActive = a.realTimeStatus === 'busy' || a.realTimeStatus === 'retry';
|
|
@@ -1014,531 +911,9 @@ async function getRemoteNodeSessionsResult(
|
|
|
1014
911
|
}
|
|
1015
912
|
}
|
|
1016
913
|
|
|
1017
|
-
async function getLocalSessionsResult(stickyBusyDelayMs: number): Promise<SessionsRouteResult> {
|
|
1018
|
-
|
|
1019
|
-
const { processes: rawProcessHints, timedOut: processDiscoveryTimedOut } =
|
|
1020
|
-
discoverOpencodeProcessCwdsWithoutPortWithMeta();
|
|
1021
|
-
const processHintsByDirectory = new Map<string, ProcessHint>();
|
|
1022
|
-
for (const process of rawProcessHints) {
|
|
1023
|
-
if (!process.cwd || process.cwd.startsWith('/private/tmp/opencode')) {
|
|
1024
|
-
continue;
|
|
1025
|
-
}
|
|
1026
|
-
if (processHintsByDirectory.has(process.cwd)) {
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
processHintsByDirectory.set(process.cwd, {
|
|
1030
|
-
pid: process.pid,
|
|
1031
|
-
directory: process.cwd,
|
|
1032
|
-
projectName: getProjectName(process.cwd),
|
|
1033
|
-
reason: 'process_without_api_port',
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
const { ports, timedOut: portDiscoveryTimedOut } = discoverOpencodePortsWithMeta();
|
|
1038
|
-
|
|
1039
|
-
if (!ports.length) {
|
|
1040
|
-
const processHints = Array.from(processHintsByDirectory.values());
|
|
1041
|
-
|
|
1042
|
-
if (portDiscoveryTimedOut || processDiscoveryTimedOut) {
|
|
1043
|
-
return {
|
|
1044
|
-
payload: {
|
|
1045
|
-
error: 'OpenCode discovery timed out',
|
|
1046
|
-
hint: 'Host process discovery exceeded timeout. Retry shortly, or increase OPENCODE_DISCOVERY_TIMEOUT_MS.',
|
|
1047
|
-
...(processHints.length > 0 ? { processHints } : {}),
|
|
1048
|
-
},
|
|
1049
|
-
status: 503,
|
|
1050
|
-
sourceMeta: {
|
|
1051
|
-
online: false,
|
|
1052
|
-
degraded: true,
|
|
1053
|
-
reason: 'OpenCode discovery timed out',
|
|
1054
|
-
},
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (processHints.length > 0) {
|
|
1059
|
-
return {
|
|
1060
|
-
payload: { sessions: [], processHints },
|
|
1061
|
-
sourceMeta: {
|
|
1062
|
-
online: false,
|
|
1063
|
-
reason: 'OpenCode server not found',
|
|
1064
|
-
},
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
return {
|
|
1069
|
-
payload: {
|
|
1070
|
-
error: 'OpenCode server not found',
|
|
1071
|
-
hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).'
|
|
1072
|
-
},
|
|
1073
|
-
status: 503,
|
|
1074
|
-
sourceMeta: {
|
|
1075
|
-
online: false,
|
|
1076
|
-
reason: 'OpenCode server not found',
|
|
1077
|
-
},
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
try {
|
|
1082
|
-
const results = await Promise.allSettled(ports.map(async (port) => {
|
|
1083
|
-
const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
|
|
1084
|
-
const sessionsResult = await withTimeout(
|
|
1085
|
-
(signal) => client.session.list({ signal }),
|
|
1086
|
-
sessionListTimeoutMs,
|
|
1087
|
-
`session.list(${port})`
|
|
1088
|
-
);
|
|
1089
|
-
const statusResult = await withTimeout(
|
|
1090
|
-
(signal) => client.session.status({ signal }),
|
|
1091
|
-
sessionStatusTimeoutMs,
|
|
1092
|
-
`session.status(${port})`
|
|
1093
|
-
).catch(() => ({ data: {} }));
|
|
1094
|
-
return { port, client, sessions: sessionsResult.data || [], status: statusResult.data || {} };
|
|
1095
|
-
}));
|
|
1096
|
-
|
|
1097
|
-
const allSessions: SessionLike[] = [];
|
|
1098
|
-
const statusMap: Record<string, { type: 'idle' | 'busy' | 'retry' }> = {};
|
|
1099
|
-
const clientByPort: Record<number, ReturnType<typeof createOpencodeClient>> = {};
|
|
1100
|
-
const sessionPortMap: Record<string, number> = {};
|
|
1101
|
-
const failedPorts: Array<{ port: number; reason: string }> = [];
|
|
1102
|
-
|
|
1103
|
-
for (let i = 0; i < results.length; i++) {
|
|
1104
|
-
const r = results[i];
|
|
1105
|
-
const port = ports[i];
|
|
1106
|
-
if (r.status !== 'fulfilled') {
|
|
1107
|
-
failedPorts.push({
|
|
1108
|
-
port,
|
|
1109
|
-
reason: r.reason instanceof Error ? r.reason.message : String(r.reason),
|
|
1110
|
-
});
|
|
1111
|
-
continue;
|
|
1112
|
-
}
|
|
1113
|
-
allSessions.push(...r.value.sessions);
|
|
1114
|
-
Object.assign(statusMap, r.value.status);
|
|
1115
|
-
clientByPort[r.value.port] = r.value.client;
|
|
1116
|
-
for (const session of r.value.sessions as SessionLike[]) {
|
|
1117
|
-
if (!(session.id in sessionPortMap)) {
|
|
1118
|
-
sessionPortMap[session.id] = r.value.port;
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Deduplicate by session.id
|
|
1124
|
-
const seen = new Set<string>();
|
|
1125
|
-
const sessions = allSessions.filter((session) => {
|
|
1126
|
-
if (seen.has(session.id)) return false;
|
|
1127
|
-
seen.add(session.id);
|
|
1128
|
-
return true;
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
const parentSessions = sessions.filter((s) => !s.parentID);
|
|
1132
|
-
const childSessions = sessions.filter((s) => !!s.parentID);
|
|
1133
|
-
|
|
1134
|
-
const lifecycleNow = Date.now();
|
|
1135
|
-
pruneSessionForceUnarchived(lifecycleNow);
|
|
1136
|
-
pruneSessionStickyStatusBlocked(lifecycleNow);
|
|
1137
|
-
|
|
1138
|
-
for (const session of parentSessions) {
|
|
1139
|
-
if (session.time?.archived !== undefined && shouldForceSessionUnarchived(session.id, lifecycleNow)) {
|
|
1140
|
-
session.time = {
|
|
1141
|
-
...session.time,
|
|
1142
|
-
archived: undefined,
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
for (const child of childSessions) {
|
|
1148
|
-
if (child.time?.archived !== undefined && shouldForceSessionUnarchived(child.id, lifecycleNow)) {
|
|
1149
|
-
child.time = {
|
|
1150
|
-
...child.time,
|
|
1151
|
-
archived: undefined,
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
if (results.length > 0 && failedPorts.length === results.length) {
|
|
1157
|
-
pruneStickyState(Date.now(), new Set<string>());
|
|
1158
|
-
return {
|
|
1159
|
-
payload: {
|
|
1160
|
-
error: 'Failed to fetch sessions from OpenCode ports',
|
|
1161
|
-
hint: 'All discovered OpenCode API ports timed out or failed. Retry shortly or increase OPENCODE_SESSIONS_LIST_TIMEOUT_MS.',
|
|
1162
|
-
failedPorts,
|
|
1163
|
-
},
|
|
1164
|
-
status: 503,
|
|
1165
|
-
sourceMeta: {
|
|
1166
|
-
online: false,
|
|
1167
|
-
degraded: true,
|
|
1168
|
-
reason: 'Failed to fetch sessions from OpenCode ports',
|
|
1169
|
-
},
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
if (failedPorts.length > 0 && parentSessions.length === 0 && childSessions.length === 0) {
|
|
1174
|
-
pruneStickyState(Date.now(), new Set<string>());
|
|
1175
|
-
const processHints = Array.from(processHintsByDirectory.values());
|
|
1176
|
-
return {
|
|
1177
|
-
payload: {
|
|
1178
|
-
sessions: [],
|
|
1179
|
-
processHints,
|
|
1180
|
-
failedPorts,
|
|
1181
|
-
degraded: true,
|
|
1182
|
-
},
|
|
1183
|
-
sourceMeta: {
|
|
1184
|
-
online: true,
|
|
1185
|
-
degraded: true,
|
|
1186
|
-
},
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// Enrich parent sessions
|
|
1191
|
-
const enrichedSessions: EnrichedSession[] = parentSessions.map((session) => {
|
|
1192
|
-
const projectName = getProjectName(session.directory);
|
|
1193
|
-
const branch = getGitBranch(session.directory);
|
|
1194
|
-
return {
|
|
1195
|
-
...session,
|
|
1196
|
-
projectName,
|
|
1197
|
-
branch,
|
|
1198
|
-
realTimeStatus: statusMap[session.id]?.type || 'idle',
|
|
1199
|
-
waitingForUser: false,
|
|
1200
|
-
children: [],
|
|
1201
|
-
};
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
const parentById = new Map(enrichedSessions.map((session) => [session.id, session]));
|
|
1205
|
-
|
|
1206
|
-
const now = Date.now();
|
|
1207
|
-
const unresolvedChildren: Array<{ parentId: string; child: SessionLike; childUpdatedAt: number }> = [];
|
|
1208
|
-
|
|
1209
|
-
// Enrich and nest child sessions under parents
|
|
1210
|
-
for (const child of childSessions) {
|
|
1211
|
-
// Find parent by parentID
|
|
1212
|
-
let parent = child.parentID
|
|
1213
|
-
? enrichedSessions.find((session) => session.id === child.parentID)
|
|
1214
|
-
: null;
|
|
1215
|
-
|
|
1216
|
-
if (!parent) {
|
|
1217
|
-
const candidates = enrichedSessions
|
|
1218
|
-
.filter((session) => session.directory === child.directory)
|
|
1219
|
-
.sort((a, b) => getUpdatedAt(b) - getUpdatedAt(a));
|
|
1220
|
-
|
|
1221
|
-
parent =
|
|
1222
|
-
candidates.find((session) => session.realTimeStatus === 'busy' || session.realTimeStatus === 'retry') ||
|
|
1223
|
-
candidates[0];
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
if (!parent) {
|
|
1227
|
-
continue;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
const statusFromMap = statusMap[child.id]?.type;
|
|
1231
|
-
const childUpdatedAt = getUpdatedAt(child);
|
|
1232
|
-
const isRecent = childUpdatedAt > 0 && now - childUpdatedAt <= CHILD_ACTIVE_WINDOW_MS;
|
|
1233
|
-
const shouldSkipArchivedChild = !!child.time?.archived && !statusFromMap && !isRecent;
|
|
1234
|
-
|
|
1235
|
-
if (shouldSkipArchivedChild) {
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
if (statusFromMap && statusFromMap !== 'idle') {
|
|
1240
|
-
parent.children.push(toChildEntry(child, statusFromMap));
|
|
1241
|
-
} else if (isRecent) {
|
|
1242
|
-
if (unresolvedChildren.length < CHILD_STATUS_MESSAGE_CHECK_LIMIT) {
|
|
1243
|
-
unresolvedChildren.push({ parentId: parent.id, child, childUpdatedAt });
|
|
1244
|
-
}
|
|
1245
|
-
} else {
|
|
1246
|
-
continue;
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
if (unresolvedChildren.length > 0) {
|
|
1251
|
-
const unresolvedChecks = await Promise.allSettled(
|
|
1252
|
-
unresolvedChildren.map(async ({ parentId, child, childUpdatedAt }) => {
|
|
1253
|
-
const port = sessionPortMap[child.id] ?? sessionPortMap[parentId];
|
|
1254
|
-
const client = port ? clientByPort[port] : undefined;
|
|
1255
|
-
const assumeBusyForUnknown =
|
|
1256
|
-
childUpdatedAt > 0 && now - childUpdatedAt <= CHILD_UNKNOWN_STATE_BUSY_WINDOW_MS;
|
|
1257
|
-
if (!client) {
|
|
1258
|
-
return {
|
|
1259
|
-
parentId,
|
|
1260
|
-
child,
|
|
1261
|
-
childStatus: assumeBusyForUnknown ? 'busy' as const : 'idle' as const,
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
try {
|
|
1266
|
-
const partStatuses = await fetchPartStatuses(client, child.id, sessionMessagesTimeoutMs);
|
|
1267
|
-
const hasRunningState = partStatuses.some((status) => status === 'running');
|
|
1268
|
-
const hasWaitingState = !hasRunningState && partStatuses.some(isWaitingPartStatus);
|
|
1269
|
-
const hasActiveState = hasWaitingState || hasRunningState;
|
|
1270
|
-
const recentlyActive = childUpdatedAt > 0 && now - childUpdatedAt <= 5 * 60 * 1000;
|
|
1271
|
-
|
|
1272
|
-
return {
|
|
1273
|
-
parentId,
|
|
1274
|
-
child,
|
|
1275
|
-
childWaitingForUser: hasWaitingState,
|
|
1276
|
-
childStatus: hasActiveState
|
|
1277
|
-
? 'busy' as const
|
|
1278
|
-
: recentlyActive || assumeBusyForUnknown
|
|
1279
|
-
? 'busy' as const
|
|
1280
|
-
: 'idle' as const,
|
|
1281
|
-
};
|
|
1282
|
-
} catch {
|
|
1283
|
-
return {
|
|
1284
|
-
parentId,
|
|
1285
|
-
child,
|
|
1286
|
-
childWaitingForUser: false,
|
|
1287
|
-
childStatus: assumeBusyForUnknown ? 'busy' as const : 'idle' as const,
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
})
|
|
1291
|
-
);
|
|
1292
|
-
|
|
1293
|
-
for (const check of unresolvedChecks) {
|
|
1294
|
-
if (check.status !== 'fulfilled') continue;
|
|
1295
|
-
if (check.value.childStatus === 'idle') continue;
|
|
1296
|
-
const parent = parentById.get(check.value.parentId);
|
|
1297
|
-
if (!parent) continue;
|
|
1298
|
-
parent.children.push(toChildEntry(check.value.child, check.value.childStatus, check.value.childWaitingForUser));
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
const parentStatusFallbackCandidates = enrichedSessions
|
|
1303
|
-
.filter((session) => {
|
|
1304
|
-
if (session.realTimeStatus !== 'idle') return false;
|
|
1305
|
-
const updatedAt = getUpdatedAt(session);
|
|
1306
|
-
if (updatedAt > 0 && now - updatedAt <= CHILD_ACTIVE_WINDOW_MS) return true;
|
|
1307
|
-
return !!session.time?.archived;
|
|
1308
|
-
})
|
|
1309
|
-
.sort((a, b) => getUpdatedAt(b) - getUpdatedAt(a))
|
|
1310
|
-
.slice(0, CHILD_STATUS_MESSAGE_CHECK_LIMIT);
|
|
1311
|
-
|
|
1312
|
-
if (parentStatusFallbackCandidates.length > 0) {
|
|
1313
|
-
const parentFallbackChecks = await Promise.allSettled(
|
|
1314
|
-
parentStatusFallbackCandidates.map(async (session) => {
|
|
1315
|
-
const updatedAt = getUpdatedAt(session);
|
|
1316
|
-
const assumeBusyForUnknown =
|
|
1317
|
-
updatedAt > 0 && now - updatedAt <= CHILD_UNKNOWN_STATE_BUSY_WINDOW_MS;
|
|
1318
|
-
const port = sessionPortMap[session.id];
|
|
1319
|
-
const client = port ? clientByPort[port] : undefined;
|
|
1320
|
-
|
|
1321
|
-
if (!client) {
|
|
1322
|
-
return {
|
|
1323
|
-
sessionId: session.id,
|
|
1324
|
-
status: assumeBusyForUnknown ? 'busy' as const : 'idle' as const,
|
|
1325
|
-
waitingForUser: false,
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
try {
|
|
1330
|
-
const partStatuses = await fetchPartStatuses(client, session.id, sessionMessagesTimeoutMs);
|
|
1331
|
-
const hasRunningState = partStatuses.some((status) => status === 'running');
|
|
1332
|
-
const hasWaitingState = !hasRunningState && partStatuses.some(isWaitingPartStatus);
|
|
1333
|
-
const hasCompletedState =
|
|
1334
|
-
partStatuses.length > 0 && partStatuses.every((status) => status === 'completed');
|
|
1335
|
-
const recentlyActive = hasRecentActivity(session, now);
|
|
1336
|
-
|
|
1337
|
-
return {
|
|
1338
|
-
sessionId: session.id,
|
|
1339
|
-
status: hasRunningState || hasWaitingState
|
|
1340
|
-
? 'busy' as const
|
|
1341
|
-
: hasCompletedState && !recentlyActive
|
|
1342
|
-
? 'idle' as const
|
|
1343
|
-
: assumeBusyForUnknown || recentlyActive
|
|
1344
|
-
? 'busy' as const
|
|
1345
|
-
: 'idle' as const,
|
|
1346
|
-
waitingForUser: hasWaitingState,
|
|
1347
|
-
};
|
|
1348
|
-
} catch {
|
|
1349
|
-
return {
|
|
1350
|
-
sessionId: session.id,
|
|
1351
|
-
status: assumeBusyForUnknown ? 'busy' as const : 'idle' as const,
|
|
1352
|
-
waitingForUser: false,
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
})
|
|
1356
|
-
);
|
|
1357
|
-
|
|
1358
|
-
for (const check of parentFallbackChecks) {
|
|
1359
|
-
if (check.status !== 'fulfilled') continue;
|
|
1360
|
-
if (check.value.status === 'idle') continue;
|
|
1361
|
-
const session = parentById.get(check.value.sessionId);
|
|
1362
|
-
if (!session) continue;
|
|
1363
|
-
session.realTimeStatus = check.value.status;
|
|
1364
|
-
if (check.value.waitingForUser) {
|
|
1365
|
-
session.waitingForUser = true;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
// Sort children for each parent: active first, then by updated time
|
|
1371
|
-
for (const session of enrichedSessions) {
|
|
1372
|
-
if (session.children.length > 0) {
|
|
1373
|
-
sortChildEntries(session.children);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
const sessionsForInteractionChecks = enrichedSessions.filter(
|
|
1378
|
-
(session) =>
|
|
1379
|
-
session.realTimeStatus === 'busy' ||
|
|
1380
|
-
!!session.time?.archived ||
|
|
1381
|
-
session.children.some((child) => child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry')
|
|
1382
|
-
);
|
|
1383
|
-
if (sessionsForInteractionChecks.length > 0) {
|
|
1384
|
-
const pendingChecks = await Promise.allSettled(
|
|
1385
|
-
sessionsForInteractionChecks.map(async (session) => {
|
|
1386
|
-
const port = sessionPortMap[session.id];
|
|
1387
|
-
const client = port ? clientByPort[port] : undefined;
|
|
1388
|
-
if (!client) {
|
|
1389
|
-
return {
|
|
1390
|
-
sessionId: session.id,
|
|
1391
|
-
parentWaiting: false,
|
|
1392
|
-
waiting: false,
|
|
1393
|
-
running: false,
|
|
1394
|
-
waitingChildIds: new Set<string>(),
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
try {
|
|
1399
|
-
const partStatuses = await fetchPartStatuses(client, session.id, sessionMessagesTimeoutMs);
|
|
1400
|
-
const hasRunning = partStatuses.some((status) => status === 'running');
|
|
1401
|
-
const hasInteractionWait = !hasRunning && partStatuses.some(isWaitingPartStatus);
|
|
1402
|
-
|
|
1403
|
-
const childStateChecks = await Promise.allSettled(
|
|
1404
|
-
session.children
|
|
1405
|
-
.filter((child) => child.realTimeStatus === 'busy' || child.realTimeStatus === 'retry')
|
|
1406
|
-
.map(async (child) => {
|
|
1407
|
-
const childPort = sessionPortMap[child.id] ?? sessionPortMap[session.id];
|
|
1408
|
-
const childClient = childPort ? clientByPort[childPort] : undefined;
|
|
1409
|
-
if (!childClient) {
|
|
1410
|
-
return { childId: child.id, waiting: false };
|
|
1411
|
-
}
|
|
1412
|
-
try {
|
|
1413
|
-
const childStatuses = await fetchPartStatuses(childClient, child.id, sessionMessagesTimeoutMs);
|
|
1414
|
-
const childHasRunning = childStatuses.some((status) => status === 'running');
|
|
1415
|
-
return {
|
|
1416
|
-
childId: child.id,
|
|
1417
|
-
waiting: !childHasRunning && childStatuses.some(isWaitingPartStatus),
|
|
1418
|
-
};
|
|
1419
|
-
} catch {
|
|
1420
|
-
return { childId: child.id, waiting: false };
|
|
1421
|
-
}
|
|
1422
|
-
})
|
|
1423
|
-
);
|
|
1424
|
-
|
|
1425
|
-
const waitingChildIds = new Set(
|
|
1426
|
-
childStateChecks
|
|
1427
|
-
.filter((result): result is PromiseFulfilledResult<{ childId: string; waiting: boolean }> => result.status === 'fulfilled')
|
|
1428
|
-
.filter((result) => result.value.waiting)
|
|
1429
|
-
.map((result) => result.value.childId)
|
|
1430
|
-
);
|
|
1431
|
-
|
|
1432
|
-
const hasWaitingChildren =
|
|
1433
|
-
waitingChildIds.size > 0 ||
|
|
1434
|
-
session.children.some((child) => child.waitingForUser || child.realTimeStatus === 'retry');
|
|
1435
|
-
|
|
1436
|
-
return {
|
|
1437
|
-
sessionId: session.id,
|
|
1438
|
-
parentWaiting: hasInteractionWait,
|
|
1439
|
-
waiting: hasInteractionWait || hasWaitingChildren,
|
|
1440
|
-
running: hasRunning,
|
|
1441
|
-
waitingChildIds,
|
|
1442
|
-
};
|
|
1443
|
-
} catch {
|
|
1444
|
-
return {
|
|
1445
|
-
sessionId: session.id,
|
|
1446
|
-
parentWaiting: false,
|
|
1447
|
-
waiting: false,
|
|
1448
|
-
running: false,
|
|
1449
|
-
waitingChildIds: new Set<string>(),
|
|
1450
|
-
};
|
|
1451
|
-
}
|
|
1452
|
-
})
|
|
1453
|
-
);
|
|
1454
|
-
|
|
1455
|
-
for (const result of pendingChecks) {
|
|
1456
|
-
if (result.status === 'fulfilled') {
|
|
1457
|
-
const session = enrichedSessions.find((candidate) => candidate.id === result.value.sessionId);
|
|
1458
|
-
if (!session) continue;
|
|
1459
|
-
for (const child of session.children) {
|
|
1460
|
-
if (result.value.waitingChildIds.has(child.id)) {
|
|
1461
|
-
child.waitingForUser = true;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
if (result.value.running) {
|
|
1465
|
-
session.realTimeStatus = 'busy';
|
|
1466
|
-
}
|
|
1467
|
-
if (result.value.parentWaiting) {
|
|
1468
|
-
session.waitingForUser = true;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
const stickyNow = Date.now();
|
|
1475
|
-
const activeStickyIds = new Set<string>();
|
|
1476
|
-
|
|
1477
|
-
for (const session of enrichedSessions) {
|
|
1478
|
-
activeStickyIds.add(session.id);
|
|
1479
|
-
for (const child of session.children) {
|
|
1480
|
-
activeStickyIds.add(`child:${child.id}`);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
for (const session of enrichedSessions) {
|
|
1485
|
-
if (shouldSkipSessionStatusStabilization(session, stickyNow)) {
|
|
1486
|
-
continue;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
applyStickyStatusStabilization(session, stickyNow, stickyBusyDelayMs);
|
|
1490
|
-
}
|
|
1491
|
-
pruneStickyState(stickyNow, activeStickyIds);
|
|
1492
|
-
|
|
1493
|
-
const knownDirectories = new Set<string>();
|
|
1494
|
-
for (const session of sessions) {
|
|
1495
|
-
if (session.directory) {
|
|
1496
|
-
knownDirectories.add(session.directory);
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
const processHints = Array.from(processHintsByDirectory.values()).filter(
|
|
1501
|
-
(hint) => !knownDirectories.has(hint.directory)
|
|
1502
|
-
);
|
|
1503
|
-
|
|
1504
|
-
const payload: SessionsSuccessPayload = {
|
|
1505
|
-
sessions: enrichedSessions,
|
|
1506
|
-
processHints,
|
|
1507
|
-
};
|
|
1508
|
-
|
|
1509
|
-
if (failedPorts.length > 0) {
|
|
1510
|
-
payload.failedPorts = failedPorts;
|
|
1511
|
-
payload.degraded = true;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
return {
|
|
1515
|
-
payload,
|
|
1516
|
-
sourceMeta: {
|
|
1517
|
-
online: true,
|
|
1518
|
-
...(failedPorts.length > 0 ? { degraded: true } : {}),
|
|
1519
|
-
},
|
|
1520
|
-
};
|
|
1521
|
-
} catch (error) {
|
|
1522
|
-
console.error('Error fetching sessions:', error);
|
|
1523
|
-
return {
|
|
1524
|
-
payload: {
|
|
1525
|
-
error: 'Failed to fetch sessions',
|
|
1526
|
-
details: error instanceof Error ? error.message : String(error),
|
|
1527
|
-
hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).'
|
|
1528
|
-
},
|
|
1529
|
-
status: 500,
|
|
1530
|
-
sourceMeta: {
|
|
1531
|
-
online: false,
|
|
1532
|
-
degraded: true,
|
|
1533
|
-
reason: 'Failed to fetch sessions',
|
|
1534
|
-
},
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
914
|
async function handleGet() {
|
|
1540
915
|
const stickyBusyDelayMs = await readStickyBusyDelayMs();
|
|
1541
|
-
return toRouteResponse(await getLocalSessionsResult(stickyBusyDelayMs));
|
|
916
|
+
return toRouteResponse(await getLocalSessionsResult({ stickyBusyDelayMs, providers: [...LOCAL_POLLING_PROVIDERS] }));
|
|
1542
917
|
}
|
|
1543
918
|
|
|
1544
919
|
async function handlePost(request: Request) {
|
|
@@ -1568,7 +943,7 @@ async function handlePost(request: Request) {
|
|
|
1568
943
|
|
|
1569
944
|
if (enabledSources.length === 1 && enabledSources[0].hostKind === 'local') {
|
|
1570
945
|
const stickyBusyDelayMs = await readStickyBusyDelayMs();
|
|
1571
|
-
const localResult = await getLocalSessionsResult(stickyBusyDelayMs);
|
|
946
|
+
const localResult = await getLocalSessionsResult({ stickyBusyDelayMs, providers: [...LOCAL_POLLING_PROVIDERS] });
|
|
1572
947
|
const rawLocalMeta = localResult.sourceMeta ?? {
|
|
1573
948
|
online: !localResult.status,
|
|
1574
949
|
...(localResult.status ? { degraded: true } : {}),
|
|
@@ -1634,7 +1009,7 @@ async function handlePost(request: Request) {
|
|
|
1634
1009
|
resolvedSources.map(async (source) => ({
|
|
1635
1010
|
source,
|
|
1636
1011
|
result: source.hostKind === 'local'
|
|
1637
|
-
? await getLocalSessionsResult(stickyBusyDelayMs)
|
|
1012
|
+
? await getLocalSessionsResult({ stickyBusyDelayMs, providers: [...LOCAL_POLLING_PROVIDERS] })
|
|
1638
1013
|
: await getRemoteNodeSessionsResult(source, nodeRecordsById.get(source.hostId)),
|
|
1639
1014
|
}))
|
|
1640
1015
|
);
|
|
@@ -1686,7 +1061,7 @@ async function handlePost(request: Request) {
|
|
|
1686
1061
|
}
|
|
1687
1062
|
|
|
1688
1063
|
return Response.json({
|
|
1689
|
-
sessions: aggregateSessions,
|
|
1064
|
+
sessions: rebuildAggregateClaudeTopology(aggregateSessions),
|
|
1690
1065
|
processHints: aggregateProcessHints,
|
|
1691
1066
|
hosts: hostStatuses,
|
|
1692
1067
|
hostStatuses,
|