vibepulse 0.2.2 → 0.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/server/app/api/node/sessions/route.js +5 -3
- package/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
- package/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
- 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 +3 -2
- 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 +1 -1
- 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/route.js +4 -2
- 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]__31d19c5c._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__31d19c5c._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__56f5f249._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__98073dd6._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js.map +1 -0
- 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]__f441109e._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__f441109e._.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_2edc9589.js +3 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js.map +1 -0
- 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_d0c0f338.js +3 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.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/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/node/sessions/route.js +5 -3
- package/.next/standalone/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
- 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.nft.json +1 -1
- 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.nft.json +1 -1
- 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.nft.json +1 -1
- 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 +3 -2
- 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 +1 -1
- 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/route.js +4 -2
- 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]__31d19c5c._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b7b717eb._.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]__f441109e._.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_2edc9589.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
- 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_d0c0f338.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/b3bc362202331708.css +3 -0
- package/.next/standalone/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
- package/.next/standalone/README.md +29 -5
- 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/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- 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/route.test.ts +282 -0
- package/.next/standalone/src/app/api/node/sessions/route.ts +141 -17
- package/.next/standalone/src/app/api/opencode-events/route.test.ts +3 -1
- package/.next/standalone/src/app/api/sessions/[id]/archive/route.test.ts +101 -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 +92 -0
- package/.next/standalone/src/app/api/sessions/[id]/delete/route.ts +45 -10
- 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/route.test.ts +1889 -107
- package/.next/standalone/src/app/api/sessions/route.ts +365 -981
- package/.next/standalone/src/components/KanbanBoard.test.tsx +307 -1
- package/.next/standalone/src/components/KanbanBoard.tsx +105 -18
- package/.next/standalone/src/components/ProjectCard.test.tsx +416 -2
- package/.next/standalone/src/components/ProjectCard.tsx +238 -86
- package/.next/standalone/src/components/SessionCard.test.tsx +253 -2
- package/.next/standalone/src/components/SessionCard.tsx +182 -76
- 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/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.ts +723 -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/static/chunks/b3bc362202331708.css +3 -0
- package/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
- 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 +29 -5
- package/package.json +1 -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]__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]__db285678._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__db285678._.js.map +0 -1
- 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]__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]__db285678._.js +0 -3
- package/.next/standalone/.next/static/chunks/f42202943f6742e5.css +0 -3
- package/.next/static/chunks/f42202943f6742e5.css +0 -3
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
|
@@ -1,152 +1,36 @@
|
|
|
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';
|
|
3
|
+
import {
|
|
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';
|
|
9
21
|
import {
|
|
10
22
|
clearSessionForceUnarchived,
|
|
11
|
-
markSessionForceUnarchived,
|
|
12
|
-
pruneSessionStickyStatusBlocked,
|
|
13
|
-
pruneSessionForceUnarchived,
|
|
14
|
-
shouldForceSessionUnarchived,
|
|
15
|
-
takeSessionStickyStatusBlocked,
|
|
16
23
|
} from '@/lib/sessionArchiveOverrides';
|
|
17
24
|
import { composeSourceKey, parseSourceKey } from '@/lib/hostIdentity';
|
|
18
25
|
import { createNodeRequestHeaders, NODE_PROTOCOL_VERSION } from '@/lib/nodeProtocol';
|
|
26
|
+
import { composeProviderSourceKey, detectProviderFromRawId, extractProviderRawId } from '@/lib/session-providers/providerIds';
|
|
19
27
|
import { listNodeRecords, type StoredNodeRecord } from '@/lib/nodeRegistry';
|
|
20
28
|
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
|
-
};
|
|
29
|
+
import type { BuiltInHostSource, RemoteHostConfig, SessionCapabilities, SessionProvider } from '@/types';
|
|
36
30
|
|
|
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
31
|
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
|
-
};
|
|
32
|
+
const CLAUDE_INFERRED_PARENT_MAX_CREATED_GAP_MS = 60_000;
|
|
33
|
+
const CLAUDE_INFERRED_PARENT_AMBIGUITY_GAP_MS = 5_000;
|
|
150
34
|
|
|
151
35
|
const LOCAL_SOURCE: BuiltInHostSource = {
|
|
152
36
|
hostId: 'local',
|
|
@@ -154,8 +38,16 @@ const LOCAL_SOURCE: BuiltInHostSource = {
|
|
|
154
38
|
hostKind: 'local',
|
|
155
39
|
};
|
|
156
40
|
|
|
41
|
+
const LOCAL_POLLING_PROVIDERS = [opencodeLocalSessionProvider, claudeCodeLocalSessionProvider] as const;
|
|
42
|
+
|
|
157
43
|
export const dynamic = 'force-dynamic';
|
|
158
44
|
|
|
45
|
+
export {
|
|
46
|
+
applyStickyBusyStatus,
|
|
47
|
+
applyStickyStatusStabilization,
|
|
48
|
+
shouldSkipSessionStatusStabilization,
|
|
49
|
+
};
|
|
50
|
+
|
|
159
51
|
export async function GET() {
|
|
160
52
|
return handleGet();
|
|
161
53
|
}
|
|
@@ -164,14 +56,6 @@ export async function POST(request: Request) {
|
|
|
164
56
|
return handlePost(request);
|
|
165
57
|
}
|
|
166
58
|
|
|
167
|
-
type MessageStateStatus = string;
|
|
168
|
-
|
|
169
|
-
type MessagePart = {
|
|
170
|
-
state?: {
|
|
171
|
-
status?: unknown;
|
|
172
|
-
};
|
|
173
|
-
};
|
|
174
|
-
|
|
175
59
|
function readPositiveTimeoutEnv(name: string, fallback: number): number {
|
|
176
60
|
const raw = process.env[name];
|
|
177
61
|
const parsed = Number(raw);
|
|
@@ -181,300 +65,6 @@ function readPositiveTimeoutEnv(name: string, fallback: number): number {
|
|
|
181
65
|
return fallback;
|
|
182
66
|
}
|
|
183
67
|
|
|
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
68
|
async function readStickyBusyDelayMs(): Promise<number> {
|
|
479
69
|
let stickyBusyDelayMs = 1000; // default 1s
|
|
480
70
|
try {
|
|
@@ -502,6 +92,23 @@ function isRemoteSource(source: SessionSource): source is RemoteHostConfig & { h
|
|
|
502
92
|
return source.hostKind === 'remote';
|
|
503
93
|
}
|
|
504
94
|
|
|
95
|
+
function getRemoteClaudeCapabilities(
|
|
96
|
+
capabilities: SessionCapabilities | undefined,
|
|
97
|
+
provider: SessionProvider,
|
|
98
|
+
source: SessionSource
|
|
99
|
+
): SessionCapabilities | undefined {
|
|
100
|
+
if (!isRemoteSource(source) || provider !== 'claude-code') {
|
|
101
|
+
return capabilities;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
openProject: capabilities?.openProject ?? true,
|
|
106
|
+
openEditor: false,
|
|
107
|
+
archive: false,
|
|
108
|
+
delete: false,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
505
112
|
function normalizeNodeBaseUrl(baseUrl: string): string | null {
|
|
506
113
|
const trimmed = baseUrl.trim();
|
|
507
114
|
if (!trimmed) {
|
|
@@ -551,10 +158,26 @@ function isTimeValue(value: unknown): boolean {
|
|
|
551
158
|
return (
|
|
552
159
|
typeof created === 'number' &&
|
|
553
160
|
typeof updated === 'number' &&
|
|
554
|
-
(archived === undefined || typeof archived === 'number')
|
|
161
|
+
(archived === undefined || archived === null || typeof archived === 'number')
|
|
555
162
|
);
|
|
556
163
|
}
|
|
557
164
|
|
|
165
|
+
function normalizeSessionTimeValue<T extends { created: number; updated: number; archived?: number } | undefined>(time: T): T {
|
|
166
|
+
if (!time) {
|
|
167
|
+
return time;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const archived = (time as { archived?: number | null }).archived;
|
|
171
|
+
if (archived === null) {
|
|
172
|
+
return {
|
|
173
|
+
created: time.created,
|
|
174
|
+
updated: time.updated,
|
|
175
|
+
} as T;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return time;
|
|
179
|
+
}
|
|
180
|
+
|
|
558
181
|
function isChildEntryValue(value: unknown): value is ChildEntry {
|
|
559
182
|
if (!isRecord(value)) {
|
|
560
183
|
return false;
|
|
@@ -784,6 +407,10 @@ function toRawSessionId(value: string): string {
|
|
|
784
407
|
}
|
|
785
408
|
}
|
|
786
409
|
|
|
410
|
+
function toProviderRawSessionId(value: string): string {
|
|
411
|
+
return extractProviderRawId(toRawSessionId(value));
|
|
412
|
+
}
|
|
413
|
+
|
|
787
414
|
function composeSourceKeySafely(hostId: string, sessionId: string): string | undefined {
|
|
788
415
|
try {
|
|
789
416
|
return composeSourceKey(hostId, sessionId);
|
|
@@ -792,17 +419,58 @@ function composeSourceKeySafely(hostId: string, sessionId: string): string | und
|
|
|
792
419
|
}
|
|
793
420
|
}
|
|
794
421
|
|
|
795
|
-
function
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
422
|
+
function composeProviderSourceKeySafely(
|
|
423
|
+
hostId: string,
|
|
424
|
+
rawId: string,
|
|
425
|
+
readOnly?: boolean,
|
|
426
|
+
provider?: SessionProvider
|
|
427
|
+
): string | undefined {
|
|
428
|
+
try {
|
|
429
|
+
return composeProviderSourceKey(hostId, rawId, {
|
|
430
|
+
readOnly,
|
|
431
|
+
...(provider ? { provider } : {}),
|
|
432
|
+
}).sourceKey;
|
|
433
|
+
} catch {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function addHostMetadataToChildEntry(
|
|
439
|
+
child: ChildEntry,
|
|
440
|
+
source: SessionSource,
|
|
441
|
+
parentSourceSessionKey?: string
|
|
442
|
+
): ChildEntry | null {
|
|
443
|
+
const rawSessionId = child.rawSessionId ?? toProviderRawSessionId(child.id);
|
|
444
|
+
const rawParentId = child.parentID ? toProviderRawSessionId(child.parentID) : child.parentID;
|
|
445
|
+
const inferredProvider = detectProviderFromRawId(child.id);
|
|
446
|
+
const parentProvider = parentSourceSessionKey ? detectProviderFromRawId(parentSourceSessionKey) : undefined;
|
|
447
|
+
const childProvider = inferredProvider === 'claude-code' ? inferredProvider : (parentProvider ?? inferredProvider);
|
|
448
|
+
const childCapabilities = getRemoteClaudeCapabilities(child.capabilities, childProvider, source);
|
|
449
|
+
const sourceSessionKey = composeProviderSourceKeySafely(source.hostId, rawSessionId, child.readOnly, childProvider);
|
|
799
450
|
if (!sourceSessionKey) {
|
|
800
451
|
return null;
|
|
801
452
|
}
|
|
802
453
|
|
|
454
|
+
const parentSourceRawId = parentSourceSessionKey
|
|
455
|
+
? toProviderRawSessionId(parentSourceSessionKey)
|
|
456
|
+
: undefined;
|
|
457
|
+
const shouldReuseParentSourceKey =
|
|
458
|
+
typeof rawParentId === 'string'
|
|
459
|
+
&& typeof parentSourceSessionKey === 'string'
|
|
460
|
+
&& parentSourceRawId === rawParentId;
|
|
461
|
+
|
|
803
462
|
const sourceParentKey = rawParentId
|
|
804
|
-
? (
|
|
463
|
+
? (
|
|
464
|
+
shouldReuseParentSourceKey
|
|
465
|
+
? parentSourceSessionKey
|
|
466
|
+
: composeProviderSourceKeySafely(source.hostId, rawParentId, undefined, childProvider) ?? undefined
|
|
467
|
+
)
|
|
805
468
|
: undefined;
|
|
469
|
+
const normalizedChildProvider = child.provider ?? (childProvider === 'claude-code' ? childProvider : undefined);
|
|
470
|
+
const normalizedChildProviderRawId =
|
|
471
|
+
normalizedChildProvider === 'claude-code'
|
|
472
|
+
? (child.providerRawId ?? rawSessionId)
|
|
473
|
+
: child.providerRawId;
|
|
806
474
|
|
|
807
475
|
return {
|
|
808
476
|
...child,
|
|
@@ -812,26 +480,37 @@ function addHostMetadataToChildEntry(child: ChildEntry, source: SessionSource):
|
|
|
812
480
|
hostLabel: source.hostLabel,
|
|
813
481
|
hostKind: source.hostKind,
|
|
814
482
|
...(isRemoteSource(source) ? { hostBaseUrl: source.baseUrl } : {}),
|
|
483
|
+
time: normalizeSessionTimeValue(child.time),
|
|
815
484
|
rawSessionId,
|
|
816
485
|
sourceSessionKey,
|
|
817
|
-
readOnly: false,
|
|
486
|
+
readOnly: child.readOnly ?? false,
|
|
487
|
+
capabilities: childCapabilities,
|
|
488
|
+
...(normalizedChildProvider ? { provider: normalizedChildProvider } : {}),
|
|
489
|
+
...(normalizedChildProviderRawId ? { providerRawId: normalizedChildProviderRawId } : {}),
|
|
818
490
|
};
|
|
819
491
|
}
|
|
820
492
|
|
|
821
493
|
function addHostMetadataToSession(session: EnrichedSession, source: SessionSource): EnrichedSession | null {
|
|
822
|
-
const rawSessionId = session.rawSessionId ??
|
|
823
|
-
const rawParentId = session.parentID ?
|
|
824
|
-
const
|
|
494
|
+
const rawSessionId = session.rawSessionId ?? toProviderRawSessionId(session.id);
|
|
495
|
+
const rawParentId = session.parentID ? toProviderRawSessionId(session.parentID) : session.parentID;
|
|
496
|
+
const sessionProvider = detectProviderFromRawId(session.id);
|
|
497
|
+
const sessionCapabilities = getRemoteClaudeCapabilities(session.capabilities, sessionProvider, source);
|
|
498
|
+
const sourceSessionKey = composeProviderSourceKeySafely(source.hostId, rawSessionId, session.readOnly, sessionProvider);
|
|
825
499
|
if (!sourceSessionKey) {
|
|
826
500
|
return null;
|
|
827
501
|
}
|
|
828
502
|
|
|
829
503
|
const sourceParentKey = rawParentId
|
|
830
|
-
? (
|
|
504
|
+
? (composeProviderSourceKeySafely(source.hostId, rawParentId, undefined, sessionProvider) ?? undefined)
|
|
831
505
|
: undefined;
|
|
506
|
+
const normalizedSessionProvider = session.provider ?? (sessionProvider === 'claude-code' ? sessionProvider : undefined);
|
|
507
|
+
const normalizedSessionProviderRawId =
|
|
508
|
+
normalizedSessionProvider === 'claude-code'
|
|
509
|
+
? (session.providerRawId ?? rawSessionId)
|
|
510
|
+
: session.providerRawId;
|
|
832
511
|
const children: ChildEntry[] = [];
|
|
833
512
|
for (const child of session.children) {
|
|
834
|
-
const enrichedChild = addHostMetadataToChildEntry(child, source);
|
|
513
|
+
const enrichedChild = addHostMetadataToChildEntry(child, source, sourceSessionKey);
|
|
835
514
|
if (enrichedChild) {
|
|
836
515
|
children.push(enrichedChild);
|
|
837
516
|
}
|
|
@@ -845,9 +524,13 @@ function addHostMetadataToSession(session: EnrichedSession, source: SessionSourc
|
|
|
845
524
|
hostLabel: source.hostLabel,
|
|
846
525
|
hostKind: source.hostKind,
|
|
847
526
|
...(isRemoteSource(source) ? { hostBaseUrl: source.baseUrl } : {}),
|
|
527
|
+
time: normalizeSessionTimeValue(session.time),
|
|
848
528
|
rawSessionId,
|
|
849
529
|
sourceSessionKey,
|
|
850
|
-
readOnly: false,
|
|
530
|
+
readOnly: session.readOnly ?? false,
|
|
531
|
+
capabilities: sessionCapabilities,
|
|
532
|
+
...(normalizedSessionProvider ? { provider: normalizedSessionProvider } : {}),
|
|
533
|
+
...(normalizedSessionProviderRawId ? { providerRawId: normalizedSessionProviderRawId } : {}),
|
|
851
534
|
children,
|
|
852
535
|
};
|
|
853
536
|
}
|
|
@@ -873,11 +556,234 @@ function addHostMetadataToPayload(payload: Record<string, unknown>, source: Sess
|
|
|
873
556
|
|
|
874
557
|
return {
|
|
875
558
|
...payload,
|
|
876
|
-
sessions,
|
|
559
|
+
sessions: rebuildAggregateClaudeTopology(sessions),
|
|
877
560
|
...(payloadDegraded || droppedSessions > 0 ? { degraded: true } : {}),
|
|
878
561
|
};
|
|
879
562
|
}
|
|
880
563
|
|
|
564
|
+
function toAggregateClaudeChildEntry(session: EnrichedSession): ChildEntry {
|
|
565
|
+
const { children: _children, ...childEntry } = session;
|
|
566
|
+
return childEntry as ChildEntry;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function hasClaudeProvider(entry: HostAwareFields): boolean {
|
|
570
|
+
return entry.provider === 'claude-code';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function toFiniteTimestamp(value: unknown): number | undefined {
|
|
574
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function getCreatedAt(session: EnrichedSession): number | undefined {
|
|
578
|
+
return toFiniteTimestamp(session.time?.created);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function getUpdatedAt(session: EnrichedSession): number | undefined {
|
|
582
|
+
return toFiniteTimestamp(session.time?.updated);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function hasSameHostIdentity(a: HostAwareFields, b: HostAwareFields): boolean {
|
|
586
|
+
return a.hostId === b.hostId && a.hostKind === b.hostKind;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
type InferredClaudeParentCandidate = {
|
|
590
|
+
parent: EnrichedSession;
|
|
591
|
+
createdGapMs: number;
|
|
592
|
+
updatedGapMs: number;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
function inferClaudeParentSession(
|
|
596
|
+
child: EnrichedSession,
|
|
597
|
+
sessions: EnrichedSession[],
|
|
598
|
+
absorbedChildIds: Set<string>
|
|
599
|
+
): EnrichedSession | undefined {
|
|
600
|
+
if (child.topology?.childSessions === 'authoritative') {
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const childCreatedAt = getCreatedAt(child);
|
|
605
|
+
if (childCreatedAt === undefined) {
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const childUpdatedAt = getUpdatedAt(child) ?? childCreatedAt;
|
|
610
|
+
const candidates: InferredClaudeParentCandidate[] = [];
|
|
611
|
+
|
|
612
|
+
for (const parent of sessions) {
|
|
613
|
+
if (parent.id === child.id || absorbedChildIds.has(parent.id)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!hasClaudeProvider(parent) || !hasSameHostIdentity(parent, child)) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (typeof parent.parentID === 'string') {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (parent.directory !== child.directory || parent.projectName !== child.projectName) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const parentCreatedAt = getCreatedAt(parent);
|
|
630
|
+
if (parentCreatedAt === undefined || parentCreatedAt > childCreatedAt) {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const createdGapMs = childCreatedAt - parentCreatedAt;
|
|
635
|
+
if (createdGapMs > CLAUDE_INFERRED_PARENT_MAX_CREATED_GAP_MS) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const parentLooksActive =
|
|
640
|
+
parent.realTimeStatus === 'busy' ||
|
|
641
|
+
parent.realTimeStatus === 'retry' ||
|
|
642
|
+
parent.waitingForUser;
|
|
643
|
+
if (!parentLooksActive) {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const parentUpdatedAt = getUpdatedAt(parent) ?? parentCreatedAt;
|
|
648
|
+
const updatedGapMs = Math.abs(childUpdatedAt - parentUpdatedAt);
|
|
649
|
+
|
|
650
|
+
candidates.push({
|
|
651
|
+
parent,
|
|
652
|
+
createdGapMs,
|
|
653
|
+
updatedGapMs,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (candidates.length === 0) {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
candidates.sort((a, b) => {
|
|
662
|
+
if (a.createdGapMs !== b.createdGapMs) {
|
|
663
|
+
return a.createdGapMs - b.createdGapMs;
|
|
664
|
+
}
|
|
665
|
+
return a.updatedGapMs - b.updatedGapMs;
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const bestCandidate = candidates[0];
|
|
669
|
+
const secondCandidate = candidates[1];
|
|
670
|
+
|
|
671
|
+
if (
|
|
672
|
+
secondCandidate
|
|
673
|
+
&& Math.abs(secondCandidate.createdGapMs - bestCandidate.createdGapMs) <= CLAUDE_INFERRED_PARENT_AMBIGUITY_GAP_MS
|
|
674
|
+
) {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return bestCandidate.parent;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function rebuildAggregateClaudeTopology(sessions: EnrichedSession[]): EnrichedSession[] {
|
|
682
|
+
const sessionsById = new Map(sessions.map((session) => [session.id, session]));
|
|
683
|
+
const absorbedChildIds = new Set<string>();
|
|
684
|
+
const resolveVisibleClaudeParent = (
|
|
685
|
+
childSession: EnrichedSession,
|
|
686
|
+
parentSession: EnrichedSession
|
|
687
|
+
): EnrichedSession | undefined => {
|
|
688
|
+
let cursor: EnrichedSession | undefined = parentSession;
|
|
689
|
+
const visited = new Set<string>([childSession.id]);
|
|
690
|
+
|
|
691
|
+
while (cursor) {
|
|
692
|
+
if (visited.has(cursor.id)) {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
visited.add(cursor.id);
|
|
697
|
+
|
|
698
|
+
if (!absorbedChildIds.has(cursor.id)) {
|
|
699
|
+
return cursor;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (typeof cursor.parentID !== 'string') {
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
cursor = sessionsById.get(cursor.parentID);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return undefined;
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const orderedSessions = [...sessions].sort((a, b) => {
|
|
713
|
+
const createdDiff = (getCreatedAt(b) ?? 0) - (getCreatedAt(a) ?? 0);
|
|
714
|
+
if (createdDiff !== 0) {
|
|
715
|
+
return createdDiff;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return (getUpdatedAt(b) ?? 0) - (getUpdatedAt(a) ?? 0);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
for (const session of orderedSessions) {
|
|
722
|
+
if (!hasClaudeProvider(session) || absorbedChildIds.has(session.id)) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let parentSession: EnrichedSession | undefined;
|
|
727
|
+
|
|
728
|
+
if (typeof session.parentID === 'string') {
|
|
729
|
+
const explicitParentSession = sessionsById.get(session.parentID);
|
|
730
|
+
if (explicitParentSession) {
|
|
731
|
+
if (
|
|
732
|
+
explicitParentSession === session
|
|
733
|
+
|| !hasClaudeProvider(explicitParentSession)
|
|
734
|
+
|| !hasSameHostIdentity(explicitParentSession, session)
|
|
735
|
+
) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const visibleParentSession = resolveVisibleClaudeParent(session, explicitParentSession);
|
|
740
|
+
if (!visibleParentSession) {
|
|
741
|
+
if (absorbedChildIds.has(explicitParentSession.id)) {
|
|
742
|
+
session.parentID = undefined;
|
|
743
|
+
}
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
parentSession = visibleParentSession;
|
|
748
|
+
if (session.parentID !== visibleParentSession.id) {
|
|
749
|
+
session.parentID = visibleParentSession.id;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (!parentSession) {
|
|
755
|
+
parentSession = inferClaudeParentSession(session, sessions, absorbedChildIds);
|
|
756
|
+
if (parentSession) {
|
|
757
|
+
session.parentID = parentSession.id;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (!parentSession) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (session.children.length > 0) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const childEntry = toAggregateClaudeChildEntry(session);
|
|
770
|
+
const existingChildIndex = parentSession.children.findIndex((child) => child.id === childEntry.id);
|
|
771
|
+
if (existingChildIndex >= 0) {
|
|
772
|
+
parentSession.children[existingChildIndex] = {
|
|
773
|
+
...parentSession.children[existingChildIndex],
|
|
774
|
+
...childEntry,
|
|
775
|
+
};
|
|
776
|
+
} else {
|
|
777
|
+
parentSession.children.push(childEntry);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
sortChildEntries(parentSession.children);
|
|
781
|
+
absorbedChildIds.add(session.id);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return sessions.filter((session) => !absorbedChildIds.has(session.id));
|
|
785
|
+
}
|
|
786
|
+
|
|
881
787
|
function sortChildEntries(children: ChildEntry[]): void {
|
|
882
788
|
children.sort((a, b) => {
|
|
883
789
|
const aActive = a.realTimeStatus === 'busy' || a.realTimeStatus === 'retry';
|
|
@@ -1014,531 +920,9 @@ async function getRemoteNodeSessionsResult(
|
|
|
1014
920
|
}
|
|
1015
921
|
}
|
|
1016
922
|
|
|
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
923
|
async function handleGet() {
|
|
1540
924
|
const stickyBusyDelayMs = await readStickyBusyDelayMs();
|
|
1541
|
-
return toRouteResponse(await getLocalSessionsResult(stickyBusyDelayMs));
|
|
925
|
+
return toRouteResponse(await getLocalSessionsResult({ stickyBusyDelayMs, providers: [...LOCAL_POLLING_PROVIDERS] }));
|
|
1542
926
|
}
|
|
1543
927
|
|
|
1544
928
|
async function handlePost(request: Request) {
|
|
@@ -1568,7 +952,7 @@ async function handlePost(request: Request) {
|
|
|
1568
952
|
|
|
1569
953
|
if (enabledSources.length === 1 && enabledSources[0].hostKind === 'local') {
|
|
1570
954
|
const stickyBusyDelayMs = await readStickyBusyDelayMs();
|
|
1571
|
-
const localResult = await getLocalSessionsResult(stickyBusyDelayMs);
|
|
955
|
+
const localResult = await getLocalSessionsResult({ stickyBusyDelayMs, providers: [...LOCAL_POLLING_PROVIDERS] });
|
|
1572
956
|
const rawLocalMeta = localResult.sourceMeta ?? {
|
|
1573
957
|
online: !localResult.status,
|
|
1574
958
|
...(localResult.status ? { degraded: true } : {}),
|
|
@@ -1634,7 +1018,7 @@ async function handlePost(request: Request) {
|
|
|
1634
1018
|
resolvedSources.map(async (source) => ({
|
|
1635
1019
|
source,
|
|
1636
1020
|
result: source.hostKind === 'local'
|
|
1637
|
-
? await getLocalSessionsResult(stickyBusyDelayMs)
|
|
1021
|
+
? await getLocalSessionsResult({ stickyBusyDelayMs, providers: [...LOCAL_POLLING_PROVIDERS] })
|
|
1638
1022
|
: await getRemoteNodeSessionsResult(source, nodeRecordsById.get(source.hostId)),
|
|
1639
1023
|
}))
|
|
1640
1024
|
);
|
|
@@ -1686,7 +1070,7 @@ async function handlePost(request: Request) {
|
|
|
1686
1070
|
}
|
|
1687
1071
|
|
|
1688
1072
|
return Response.json({
|
|
1689
|
-
sessions: aggregateSessions,
|
|
1073
|
+
sessions: rebuildAggregateClaudeTopology(aggregateSessions),
|
|
1690
1074
|
processHints: aggregateProcessHints,
|
|
1691
1075
|
hosts: hostStatuses,
|
|
1692
1076
|
hostStatuses,
|