@aoagents/ao-web 0.3.0 → 0.4.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-build-manifest.json +179 -187
- package/.next/app-path-routes-manifest.json +9 -10
- package/.next/build-manifest.json +6 -6
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +34 -34
- package/.next/react-loadable-manifest.json +14 -14
- package/.next/required-server-files.json +3 -3
- package/.next/server/app/_not-found/page.js +2 -2
- 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 +8 -11
- package/.next/server/app/api/backlog/route.js +1 -1
- package/.next/server/app/api/backlog/route.js.nft.json +1 -1
- package/.next/server/app/api/backlog/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/browse-directory/route.js +1 -1
- package/.next/server/app/api/browse-directory/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/browse/route.js +1 -1
- package/.next/server/app/api/filesystem/browse/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/issues/route.js +1 -1
- package/.next/server/app/api/issues/route.js.nft.json +1 -1
- package/.next/server/app/api/issues/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/observability/route.js +1 -1
- package/.next/server/app/api/observability/route.js.nft.json +1 -1
- package/.next/server/app/api/observability/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/orchestrators/route.js +1 -1
- package/.next/server/app/api/orchestrators/route.js.nft.json +1 -1
- package/.next/server/app/api/orchestrators/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/route.js +1 -1
- package/.next/server/app/api/projects/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/reload/route.js +1 -1
- package/.next/server/app/api/projects/reload/route.js.nft.json +1 -1
- package/.next/server/app/api/projects/reload/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/route.js +1 -1
- package/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/prs/[id]/merge/route.js +1 -1
- package/.next/server/app/api/prs/[id]/merge/route.js.nft.json +1 -1
- package/.next/server/app/api/prs/[id]/merge/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/runtime/terminal/route.js +1 -1
- package/.next/server/app/api/runtime/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/kill/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/kill/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/message/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/message/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/message/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/remap/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/remap/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/remap/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/restore/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/send/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/send/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/patches/route.js +1 -1
- package/.next/server/app/api/sessions/patches/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/patches/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/setup-labels/route.js +1 -1
- package/.next/server/app/api/setup-labels/route.js.nft.json +1 -1
- package/.next/server/app/api/setup-labels/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/spawn/route.js +1 -1
- package/.next/server/app/api/spawn/route.js.nft.json +1 -1
- package/.next/server/app/api/spawn/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/verify/route.js +1 -1
- package/.next/server/app/api/verify/route.js.nft.json +1 -1
- package/.next/server/app/api/verify/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/webhooks/[...slug]/route.js +1 -1
- package/.next/server/app/api/webhooks/[...slug]/route.js.nft.json +1 -1
- package/.next/server/app/api/webhooks/[...slug]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/apple-icon/route.js +1 -1
- package/.next/server/app/apple-icon/route.js.nft.json +1 -1
- package/.next/server/app/apple-icon/route_client-reference-manifest.js +1 -1
- package/.next/server/app/apple-icon.body +0 -0
- package/.next/server/app/dev/terminal-test/page.js +3 -3
- package/.next/server/app/dev/terminal-test/page.js.nft.json +1 -1
- package/.next/server/app/dev/terminal-test/page_client-reference-manifest.js +1 -1
- package/.next/server/app/dev/terminal-test.html +1 -1
- package/.next/server/app/dev/terminal-test.rsc +9 -12
- package/.next/server/app/icon/route.js +1 -1
- package/.next/server/app/icon/route.js.nft.json +1 -1
- package/.next/server/app/icon/route_client-reference-manifest.js +1 -1
- package/.next/server/app/icon-192/route.js +1 -1
- package/.next/server/app/icon-192/route.js.nft.json +1 -1
- package/.next/server/app/icon-192/route_client-reference-manifest.js +1 -1
- package/.next/server/app/icon-512/route.js +1 -1
- package/.next/server/app/icon-512/route.js.nft.json +1 -1
- package/.next/server/app/icon-512/route_client-reference-manifest.js +1 -1
- package/.next/server/app/icon.body +0 -0
- package/.next/server/app/manifest.webmanifest/route.js +2 -2
- package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
- package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
- package/.next/server/app/manifest.webmanifest.body +1 -1
- package/.next/server/app/orchestrators/page.js +2 -2
- package/.next/server/app/orchestrators/page.js.nft.json +1 -1
- package/.next/server/app/orchestrators/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[projectId]/page.js +2 -2
- package/.next/server/app/projects/[projectId]/page.js.nft.json +1 -1
- package/.next/server/app/projects/[projectId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[projectId]/sessions/[id]/page.js +2 -2
- package/.next/server/app/projects/[projectId]/sessions/[id]/page.js.nft.json +1 -1
- package/.next/server/app/projects/[projectId]/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[projectId]/settings/page.js +2 -2
- package/.next/server/app/projects/[projectId]/settings/page.js.nft.json +1 -1
- package/.next/server/app/projects/[projectId]/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/prs/page.js +2 -2
- package/.next/server/app/prs/page.js.nft.json +1 -1
- package/.next/server/app/prs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/sessions/[id]/page.js +2 -2
- package/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/test-direct/page.js +2 -2
- package/.next/server/app/test-direct/page.js.nft.json +1 -1
- package/.next/server/app/test-direct/page_client-reference-manifest.js +1 -1
- package/.next/server/app/test-direct.html +1 -1
- package/.next/server/app/test-direct.rsc +9 -12
- package/.next/server/app-paths-manifest.json +9 -10
- package/.next/server/chunks/1172.js +1 -1
- package/.next/server/chunks/1271.js +1 -1
- package/.next/server/chunks/1876.js +9 -0
- package/.next/server/chunks/3714.js +1 -1
- package/.next/server/chunks/4520.js +1 -1
- package/.next/server/chunks/7167.js +1 -0
- package/.next/server/chunks/7173.js +9 -0
- package/.next/server/chunks/8539.js +3 -0
- package/.next/server/chunks/9223.js +440 -0
- package/.next/server/chunks/9381.js +658 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/next-font-manifest.js +1 -1
- package/.next/server/next-font-manifest.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/5nmlY5IOwz-Od3p2SB2hh/_buildManifest.js +1 -0
- package/.next/static/chunks/1383.faf35b62c555f5a1.js +1 -0
- package/.next/static/chunks/3764.ba81c7b8d5b21136.js +1 -0
- package/.next/static/chunks/3780-e20898ff49ddc867.js +1 -0
- package/.next/static/chunks/4465-0c7772f1499dffa4.js +1 -0
- package/.next/static/chunks/5795-b96fd46c8c7344fc.js +1 -0
- package/.next/static/chunks/7008-6e85da85f12f9858.js +1 -0
- package/.next/static/chunks/app/_not-found/page-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/backlog/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/browse-directory/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/filesystem/browse/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/issues/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/observability/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/orchestrators/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/projects/reload/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/projects/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/prs/[id]/merge/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/runtime/terminal/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/kill/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/message/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/remap/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/restore/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/send/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/sessions/patches/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/setup-labels/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/spawn/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/verify/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/api/webhooks/[...slug]/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/apple-icon/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/dev/terminal-test/page-095511c7a110fbc9.js +1 -0
- package/.next/static/chunks/app/error-3ebcf3275d91d60a.js +1 -0
- package/.next/static/chunks/app/global-error-8f082029a4cf6af1.js +1 -0
- package/.next/static/chunks/app/icon/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/icon-192/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/icon-512/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/layout-be776dec5531d47b.js +1 -0
- package/.next/static/chunks/app/loading-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/manifest.webmanifest/route-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/{not-found-824d5d3c6e296eeb.js → not-found-0eb78483b8277b8d.js} +1 -1
- package/.next/static/chunks/app/orchestrators/page-406e530b0d4ad46f.js +1 -0
- package/.next/static/chunks/app/page-a92f3646b54545b0.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/loading-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/page-d14fdcff4998d3bf.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-96fc04db76c65761.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/settings/page-ce2e904def1bef9c.js +1 -0
- package/.next/static/chunks/app/prs/page-95dbbebb4d372535.js +1 -0
- package/.next/static/chunks/app/sessions/[id]/error-5468aace23fd8872.js +1 -0
- package/.next/static/chunks/app/sessions/[id]/loading-3b8a01e726e988c8.js +1 -0
- package/.next/static/chunks/app/sessions/[id]/{not-found-824d5d3c6e296eeb.js → not-found-0eb78483b8277b8d.js} +1 -1
- package/.next/static/chunks/app/sessions/[id]/page-8ef744531757185e.js +1 -0
- package/.next/static/chunks/app/test-direct/page-ab98dc9b04bacaa5.js +1 -0
- package/.next/static/chunks/{main-app-690acf9d5d2050c9.js → main-app-4b27cdf1baf203ad.js} +1 -1
- package/.next/static/chunks/{webpack-d4ff5ed5e153ca3e.js → webpack-ab12779265bdb8f9.js} +1 -1
- package/.next/static/css/397ea3e60645d3f3.css +1 -0
- package/dist-server/mux-websocket.js +135 -123
- package/dist-server/tmux-utils.js +99 -9
- package/next.config.js +6 -0
- package/package.json +18 -16
- package/.next/server/app/api/events/route.js +0 -9
- package/.next/server/app/api/events/route.js.nft.json +0 -1
- package/.next/server/app/api/events/route_client-reference-manifest.js +0 -1
- package/.next/server/chunks/252.js +0 -11
- package/.next/server/chunks/2810.js +0 -1
- package/.next/server/chunks/3602.js +0 -382
- package/.next/server/chunks/3667.js +0 -277
- package/.next/server/chunks/6172.js +0 -9
- package/.next/server/chunks/8367.js +0 -3
- package/.next/static/chunks/3697.4d6d86c5f0caf73e.js +0 -1
- package/.next/static/chunks/4465-aaba60a6355de914.js +0 -1
- package/.next/static/chunks/6078.47ce88bee96cfaee.js +0 -1
- package/.next/static/chunks/6607-405ce4d15e595f4a.js +0 -1
- package/.next/static/chunks/7008-71ebb186f0549f41.js +0 -1
- package/.next/static/chunks/9331-fcdd652218ac6f68.js +0 -1
- package/.next/static/chunks/app/_not-found/page-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/backlog/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/browse-directory/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/events/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/filesystem/browse/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/issues/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/observability/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/orchestrators/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/projects/reload/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/projects/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/prs/[id]/merge/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/runtime/terminal/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/kill/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/message/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/remap/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/restore/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/send/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/sessions/patches/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/setup-labels/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/spawn/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/verify/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/api/webhooks/[...slug]/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/apple-icon/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/dev/terminal-test/page-5765f0465db56fed.js +0 -1
- package/.next/static/chunks/app/error-670f1d8bf2b6859c.js +0 -1
- package/.next/static/chunks/app/global-error-ca06d2b1be2d4ae0.js +0 -1
- package/.next/static/chunks/app/icon/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/icon-192/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/icon-512/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/layout-ddf79478d9a673bb.js +0 -1
- package/.next/static/chunks/app/loading-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/manifest.webmanifest/route-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/orchestrators/page-4c190484788aaaa3.js +0 -1
- package/.next/static/chunks/app/page-d3b83ad5f09b6ec7.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/loading-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/page-9f3dfbea006747cf.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-a7090a06948f9a6b.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/settings/page-219df6dcbc6d1107.js +0 -1
- package/.next/static/chunks/app/prs/page-a285e930235a23af.js +0 -1
- package/.next/static/chunks/app/sessions/[id]/error-eb0973907da68a37.js +0 -1
- package/.next/static/chunks/app/sessions/[id]/loading-e2dea9178b4af8db.js +0 -1
- package/.next/static/chunks/app/sessions/[id]/page-98f398b822092218.js +0 -1
- package/.next/static/chunks/app/test-direct/page-f9bff7d7b4dfe728.js +0 -1
- package/.next/static/css/46a7e8e962075e54.css +0 -1
- package/.next/static/css/577ea7f5ad4f0576.css +0 -1
- package/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- package/.next/static/media/558ca1a6aa3cb55e-s.p.woff2 +0 -0
- package/.next/static/media/64d784ea54a4acde-s.woff2 +0 -0
- package/.next/static/media/6d831b18ae5b01dc-s.woff2 +0 -0
- package/.next/static/media/8d697b304b401681-s.woff2 +0 -0
- package/.next/static/media/ac0e76ddaeeb7981-s.woff2 +0 -0
- package/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- package/.next/static/media/edc640959b0c7826-s.woff2 +0 -0
- package/.next/static/media/ff71da380fbe67dd-s.woff2 +0 -0
- package/.next/static/q6yh3n8jgvyI9JIbexzuH/_buildManifest.js +0 -1
- /package/.next/static/{q6yh3n8jgvyI9JIbexzuH → 5nmlY5IOwz-Od3p2SB2hh}/_ssgManifest.js +0 -0
|
@@ -2,46 +2,77 @@
|
|
|
2
2
|
* Multiplexed WebSocket server for terminal multiplexing.
|
|
3
3
|
* Manages multiple terminal connections over a single persistent WebSocket.
|
|
4
4
|
*
|
|
5
|
-
* Session updates are delivered via
|
|
6
|
-
*
|
|
7
|
-
* This replaces per-client HTTP polling and makes session updates event-driven.
|
|
5
|
+
* Session updates are delivered via polling of Next.js /api/sessions/patches
|
|
6
|
+
* every 3s, then broadcast to all subscribed clients via WebSocket.
|
|
8
7
|
*/
|
|
9
8
|
import { WebSocketServer, WebSocket } from "ws";
|
|
10
9
|
import { homedir, userInfo } from "node:os";
|
|
11
10
|
import { spawn } from "node:child_process";
|
|
12
11
|
import { findTmux, resolveTmuxSession, validateSessionId } from "./tmux-utils.js";
|
|
13
12
|
/**
|
|
14
|
-
* Manages
|
|
15
|
-
* Broadcasts
|
|
16
|
-
* Lazily
|
|
13
|
+
* Manages polling of session patches from Next.js /api/sessions/patches.
|
|
14
|
+
* Broadcasts to all subscribed callbacks.
|
|
15
|
+
* Lazily starts polling on first subscriber, stops when the last one leaves.
|
|
17
16
|
*/
|
|
18
17
|
export class SessionBroadcaster {
|
|
19
18
|
subscribers = new Set();
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
errorSubscribers = new Set();
|
|
20
|
+
intervalId = null;
|
|
21
|
+
polling = false;
|
|
22
22
|
baseUrl;
|
|
23
23
|
constructor(nextPort) {
|
|
24
24
|
this.baseUrl = `http://localhost:${nextPort}`;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Subscribe to session patches. Returns an unsubscribe function.
|
|
28
|
-
* Sends an immediate snapshot to the new subscriber, then
|
|
27
|
+
* Subscribe to session patches and errors. Returns an unsubscribe function.
|
|
28
|
+
* Sends an immediate snapshot to the new subscriber, then polling updates.
|
|
29
29
|
*/
|
|
30
|
-
subscribe(callback) {
|
|
30
|
+
subscribe(callback, onError) {
|
|
31
31
|
const wasEmpty = this.subscribers.size === 0;
|
|
32
32
|
this.subscribers.add(callback);
|
|
33
|
+
if (onError)
|
|
34
|
+
this.errorSubscribers.add(onError);
|
|
33
35
|
// Immediately send a one-off snapshot to just this new subscriber
|
|
34
|
-
void this.fetchSnapshot().then((
|
|
35
|
-
if (sessions && this.subscribers.has(callback)) {
|
|
36
|
-
|
|
36
|
+
void this.fetchSnapshot().then((result) => {
|
|
37
|
+
if (result.sessions && this.subscribers.has(callback)) {
|
|
38
|
+
try {
|
|
39
|
+
callback(result.sessions);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Isolate subscriber errors so one bad subscriber doesn't break others
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (result.error && onError && this.errorSubscribers.has(onError)) {
|
|
46
|
+
try {
|
|
47
|
+
onError(result.error);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Isolate subscriber errors
|
|
51
|
+
}
|
|
37
52
|
}
|
|
38
53
|
});
|
|
39
|
-
// Start
|
|
54
|
+
// Start polling if this is the first subscriber
|
|
40
55
|
if (wasEmpty) {
|
|
41
|
-
|
|
56
|
+
this.intervalId = setInterval(() => {
|
|
57
|
+
if (this.polling)
|
|
58
|
+
return;
|
|
59
|
+
this.polling = true;
|
|
60
|
+
void this.fetchSnapshot()
|
|
61
|
+
.then((result) => {
|
|
62
|
+
if (result.sessions && this.intervalId !== null)
|
|
63
|
+
this.broadcast(result.sessions);
|
|
64
|
+
else if (result.error && this.intervalId !== null)
|
|
65
|
+
this.broadcastError(result.error);
|
|
66
|
+
})
|
|
67
|
+
.finally(() => {
|
|
68
|
+
this.polling = false;
|
|
69
|
+
});
|
|
70
|
+
}, 3000);
|
|
42
71
|
}
|
|
43
72
|
return () => {
|
|
44
73
|
this.subscribers.delete(callback);
|
|
74
|
+
if (onError)
|
|
75
|
+
this.errorSubscribers.delete(onError);
|
|
45
76
|
if (this.subscribers.size === 0) {
|
|
46
77
|
this.disconnect();
|
|
47
78
|
}
|
|
@@ -57,99 +88,45 @@ export class SessionBroadcaster {
|
|
|
57
88
|
}
|
|
58
89
|
}
|
|
59
90
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const controller = new AbortController();
|
|
64
|
-
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
|
91
|
+
broadcastError(error) {
|
|
92
|
+
for (const callback of this.errorSubscribers) {
|
|
65
93
|
try {
|
|
66
|
-
|
|
67
|
-
signal: controller.signal,
|
|
68
|
-
});
|
|
69
|
-
clearTimeout(timeoutId);
|
|
70
|
-
if (!res.ok)
|
|
71
|
-
return null;
|
|
72
|
-
const data = (await res.json());
|
|
73
|
-
return data.sessions ?? null;
|
|
94
|
+
callback(error);
|
|
74
95
|
}
|
|
75
|
-
catch {
|
|
76
|
-
|
|
77
|
-
return null;
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error("[MuxServer] Session error subscriber threw:", err);
|
|
78
98
|
}
|
|
79
99
|
}
|
|
80
|
-
catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
100
|
}
|
|
84
|
-
/**
|
|
85
|
-
async
|
|
86
|
-
if (this.abortController)
|
|
87
|
-
return;
|
|
101
|
+
/** One-shot HTTP fetch of the current session list. */
|
|
102
|
+
async fetchSnapshot() {
|
|
88
103
|
const controller = new AbortController();
|
|
89
|
-
|
|
90
|
-
const { signal } = controller;
|
|
104
|
+
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
|
91
105
|
try {
|
|
92
|
-
const res = await fetch(`${this.baseUrl}/api/
|
|
93
|
-
signal,
|
|
94
|
-
headers: { Accept: "text/event-stream" },
|
|
106
|
+
const res = await fetch(`${this.baseUrl}/api/sessions/patches`, {
|
|
107
|
+
signal: controller.signal,
|
|
95
108
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
let buffer = "";
|
|
102
|
-
while (!signal.aborted) {
|
|
103
|
-
const { done, value } = await reader.read();
|
|
104
|
-
if (done)
|
|
105
|
-
break;
|
|
106
|
-
buffer += decoder.decode(value, { stream: true });
|
|
107
|
-
const lines = buffer.split("\n");
|
|
108
|
-
buffer = lines.pop() ?? "";
|
|
109
|
-
for (const line of lines) {
|
|
110
|
-
if (!line.startsWith("data: "))
|
|
111
|
-
continue;
|
|
112
|
-
try {
|
|
113
|
-
const event = JSON.parse(line.slice(6));
|
|
114
|
-
if (event.type === "snapshot" && event.sessions) {
|
|
115
|
-
this.broadcast(event.sessions);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// ignore malformed events
|
|
120
|
-
}
|
|
121
|
-
}
|
|
109
|
+
clearTimeout(timeoutId);
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const msg = `Session fetch failed: HTTP ${res.status}`;
|
|
112
|
+
console.warn(`[SessionBroadcaster] ${msg}`);
|
|
113
|
+
return { sessions: null, error: msg };
|
|
122
114
|
}
|
|
115
|
+
const data = (await res.json());
|
|
116
|
+
return { sessions: data.sessions ?? null, error: null };
|
|
123
117
|
}
|
|
124
118
|
catch (err) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
console.warn("[
|
|
128
|
-
|
|
129
|
-
finally {
|
|
130
|
-
// Only clear our own controller — a concurrent connect() may have already
|
|
131
|
-
// set a new one (e.g. disconnect() → subscribe() → connect() in the same tick).
|
|
132
|
-
if (this.abortController === controller) {
|
|
133
|
-
this.abortController = null;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// Reconnect with backoff if there are still subscribers
|
|
137
|
-
if (this.subscribers.size > 0) {
|
|
138
|
-
console.log("[MuxServer] SSE reconnecting in 5s");
|
|
139
|
-
this.reconnectTimer = setTimeout(() => {
|
|
140
|
-
this.reconnectTimer = null;
|
|
141
|
-
if (this.subscribers.size > 0)
|
|
142
|
-
void this.connect();
|
|
143
|
-
}, 5000);
|
|
119
|
+
clearTimeout(timeoutId);
|
|
120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
console.warn("[SessionBroadcaster] fetchSnapshot error:", msg);
|
|
122
|
+
return { sessions: null, error: msg };
|
|
144
123
|
}
|
|
145
124
|
}
|
|
146
125
|
disconnect() {
|
|
147
|
-
if (this.
|
|
148
|
-
|
|
149
|
-
this.
|
|
126
|
+
if (this.intervalId !== null) {
|
|
127
|
+
clearInterval(this.intervalId);
|
|
128
|
+
this.intervalId = null;
|
|
150
129
|
}
|
|
151
|
-
this.abortController?.abort();
|
|
152
|
-
this.abortController = null;
|
|
153
130
|
}
|
|
154
131
|
}
|
|
155
132
|
let ptySpawn;
|
|
@@ -162,6 +139,7 @@ catch (err) {
|
|
|
162
139
|
console.warn("[MuxServer] node-pty not available — mux server will be disabled.", err);
|
|
163
140
|
}
|
|
164
141
|
const RING_BUFFER_MAX = 50 * 1024; // 50KB max per terminal
|
|
142
|
+
const WS_BUFFER_HIGH_WATERMARK = 64 * 1024; // 64KB
|
|
165
143
|
const MAX_REATTACH_ATTEMPTS = 3;
|
|
166
144
|
/**
|
|
167
145
|
* TerminalManager manages PTY processes independently of WebSocket connections.
|
|
@@ -173,21 +151,29 @@ class TerminalManager {
|
|
|
173
151
|
constructor(tmuxPath) {
|
|
174
152
|
this.TMUX = tmuxPath ?? findTmux();
|
|
175
153
|
}
|
|
154
|
+
terminalKey(id, projectId) {
|
|
155
|
+
return projectId ? `${projectId}:${id}` : id;
|
|
156
|
+
}
|
|
176
157
|
/**
|
|
177
158
|
* Open/attach to a terminal. If already open, just return.
|
|
178
159
|
* If has subscribers but PTY crashed, re-attach.
|
|
179
160
|
*/
|
|
180
|
-
open(id) {
|
|
161
|
+
open(id, projectId, tmuxName) {
|
|
181
162
|
// Validate and resolve
|
|
182
163
|
if (!validateSessionId(id)) {
|
|
183
164
|
throw new Error(`Invalid session ID: ${id}`);
|
|
184
165
|
}
|
|
185
|
-
|
|
166
|
+
// Use provided tmuxName, or reuse from existing terminal entry, or resolve
|
|
167
|
+
const key = this.terminalKey(id, projectId);
|
|
168
|
+
const existing = this.terminals.get(key);
|
|
169
|
+
const tmuxSessionId = tmuxName ??
|
|
170
|
+
existing?.tmuxSessionId ??
|
|
171
|
+
resolveTmuxSession(id, this.TMUX, undefined, undefined, projectId);
|
|
186
172
|
if (!tmuxSessionId) {
|
|
187
173
|
throw new Error(`Session not found: ${id}`);
|
|
188
174
|
}
|
|
189
175
|
// Get or create terminal entry
|
|
190
|
-
let terminal = this.terminals.get(
|
|
176
|
+
let terminal = this.terminals.get(key);
|
|
191
177
|
if (!terminal) {
|
|
192
178
|
terminal = {
|
|
193
179
|
id,
|
|
@@ -199,7 +185,7 @@ class TerminalManager {
|
|
|
199
185
|
bufferBytes: 0,
|
|
200
186
|
reattachAttempts: 0,
|
|
201
187
|
};
|
|
202
|
-
this.terminals.set(
|
|
188
|
+
this.terminals.set(key, terminal);
|
|
203
189
|
}
|
|
204
190
|
// If PTY is already attached, we're done
|
|
205
191
|
if (terminal.pty) {
|
|
@@ -271,7 +257,7 @@ class TerminalManager {
|
|
|
271
257
|
terminal.reattachAttempts += 1;
|
|
272
258
|
console.log(`[MuxServer] Re-attaching to ${id} (attempt ${terminal.reattachAttempts}/${MAX_REATTACH_ATTEMPTS})`);
|
|
273
259
|
try {
|
|
274
|
-
this.open(id);
|
|
260
|
+
this.open(id, projectId);
|
|
275
261
|
terminal.reattachAttempts = 0; // reset on successful attach
|
|
276
262
|
return; // re-attached — don't notify exit
|
|
277
263
|
}
|
|
@@ -293,8 +279,8 @@ class TerminalManager {
|
|
|
293
279
|
/**
|
|
294
280
|
* Write data to the PTY if attached
|
|
295
281
|
*/
|
|
296
|
-
write(id, data) {
|
|
297
|
-
const terminal = this.terminals.get(id);
|
|
282
|
+
write(id, data, projectId) {
|
|
283
|
+
const terminal = this.terminals.get(this.terminalKey(id, projectId));
|
|
298
284
|
if (terminal?.pty) {
|
|
299
285
|
terminal.pty.write(data);
|
|
300
286
|
}
|
|
@@ -302,8 +288,8 @@ class TerminalManager {
|
|
|
302
288
|
/**
|
|
303
289
|
* Resize the PTY if attached
|
|
304
290
|
*/
|
|
305
|
-
resize(id, cols, rows) {
|
|
306
|
-
const terminal = this.terminals.get(id);
|
|
291
|
+
resize(id, cols, rows, projectId) {
|
|
292
|
+
const terminal = this.terminals.get(this.terminalKey(id, projectId));
|
|
307
293
|
if (terminal?.pty) {
|
|
308
294
|
terminal.pty.resize(cols, rows);
|
|
309
295
|
}
|
|
@@ -313,10 +299,11 @@ class TerminalManager {
|
|
|
313
299
|
* Automatically opens the terminal if needed.
|
|
314
300
|
* @param onExit - called when the PTY exits and cannot be re-attached
|
|
315
301
|
*/
|
|
316
|
-
subscribe(id, callback, onExit) {
|
|
302
|
+
subscribe(id, projectId, callback, onExit) {
|
|
317
303
|
// Ensure terminal is open
|
|
318
|
-
this.open(id);
|
|
319
|
-
const
|
|
304
|
+
this.open(id, projectId);
|
|
305
|
+
const key = this.terminalKey(id, projectId);
|
|
306
|
+
const terminal = this.terminals.get(key);
|
|
320
307
|
if (!terminal) {
|
|
321
308
|
throw new Error(`Failed to open terminal: ${id}`);
|
|
322
309
|
}
|
|
@@ -335,15 +322,15 @@ class TerminalManager {
|
|
|
335
322
|
terminal.pty.kill();
|
|
336
323
|
terminal.pty = null;
|
|
337
324
|
}
|
|
338
|
-
this.terminals.delete(
|
|
325
|
+
this.terminals.delete(key);
|
|
339
326
|
}
|
|
340
327
|
};
|
|
341
328
|
}
|
|
342
329
|
/**
|
|
343
330
|
* Get buffered data for a terminal
|
|
344
331
|
*/
|
|
345
|
-
getBuffer(id) {
|
|
346
|
-
const terminal = this.terminals.get(id);
|
|
332
|
+
getBuffer(id, projectId) {
|
|
333
|
+
const terminal = this.terminals.get(this.terminalKey(id, projectId));
|
|
347
334
|
if (!terminal)
|
|
348
335
|
return "";
|
|
349
336
|
return terminal.buffer.join("");
|
|
@@ -400,60 +387,75 @@ export function createMuxWebSocket(tmuxPath) {
|
|
|
400
387
|
}
|
|
401
388
|
else if (msg.ch === "terminal") {
|
|
402
389
|
const { id, type } = msg;
|
|
390
|
+
const projectId = "projectId" in msg ? msg.projectId : undefined;
|
|
391
|
+
const subscriptionKey = projectId ? `${projectId}:${id}` : id;
|
|
403
392
|
try {
|
|
404
393
|
if (type === "open") {
|
|
405
394
|
// Validate session exists
|
|
406
|
-
terminalManager.open(id);
|
|
395
|
+
terminalManager.open(id, projectId, "tmuxName" in msg ? msg.tmuxName : undefined);
|
|
407
396
|
// Send opened confirmation (idempotent — safe to send on re-open)
|
|
408
|
-
const openedMsg = {
|
|
397
|
+
const openedMsg = {
|
|
398
|
+
ch: "terminal",
|
|
399
|
+
id,
|
|
400
|
+
type: "opened",
|
|
401
|
+
...(projectId && { projectId }),
|
|
402
|
+
};
|
|
409
403
|
ws.send(JSON.stringify(openedMsg));
|
|
410
404
|
// Subscribe and send history buffer only for new subscribers.
|
|
411
405
|
// Skipping the buffer on re-open prevents duplicate output when
|
|
412
406
|
// MuxProvider re-sends open for all terminals on reconnect.
|
|
413
|
-
if (!subscriptions.has(
|
|
407
|
+
if (!subscriptions.has(subscriptionKey)) {
|
|
414
408
|
// Send buffered history to catch up the new subscriber
|
|
415
|
-
const buffer = terminalManager.getBuffer(id);
|
|
409
|
+
const buffer = terminalManager.getBuffer(id, projectId);
|
|
416
410
|
if (buffer) {
|
|
417
411
|
const bufferMsg = {
|
|
418
412
|
ch: "terminal",
|
|
419
413
|
id,
|
|
420
414
|
type: "data",
|
|
421
415
|
data: buffer,
|
|
416
|
+
...(projectId && { projectId }),
|
|
422
417
|
};
|
|
423
418
|
ws.send(JSON.stringify(bufferMsg));
|
|
424
419
|
}
|
|
425
|
-
const unsub = terminalManager.subscribe(id, (data) => {
|
|
420
|
+
const unsub = terminalManager.subscribe(id, projectId, (data) => {
|
|
426
421
|
const dataMsg = {
|
|
427
422
|
ch: "terminal",
|
|
428
423
|
id,
|
|
429
424
|
type: "data",
|
|
430
425
|
data,
|
|
426
|
+
...(projectId && { projectId }),
|
|
431
427
|
};
|
|
432
428
|
if (ws.readyState === WebSocket.OPEN) {
|
|
433
429
|
ws.send(JSON.stringify(dataMsg));
|
|
434
430
|
}
|
|
435
431
|
}, (exitCode) => {
|
|
436
|
-
const exitedMsg = {
|
|
432
|
+
const exitedMsg = {
|
|
433
|
+
ch: "terminal",
|
|
434
|
+
id,
|
|
435
|
+
type: "exited",
|
|
436
|
+
code: exitCode,
|
|
437
|
+
...(projectId && { projectId }),
|
|
438
|
+
};
|
|
437
439
|
if (ws.readyState === WebSocket.OPEN) {
|
|
438
440
|
ws.send(JSON.stringify(exitedMsg));
|
|
439
441
|
}
|
|
440
442
|
});
|
|
441
|
-
subscriptions.set(
|
|
443
|
+
subscriptions.set(subscriptionKey, unsub);
|
|
442
444
|
}
|
|
443
445
|
}
|
|
444
446
|
else if (type === "data" && "data" in msg) {
|
|
445
|
-
terminalManager.write(id, msg.data);
|
|
447
|
+
terminalManager.write(id, msg.data, projectId);
|
|
446
448
|
}
|
|
447
449
|
else if (type === "resize" && "cols" in msg && "rows" in msg) {
|
|
448
|
-
terminalManager.resize(id, msg.cols, msg.rows);
|
|
450
|
+
terminalManager.resize(id, msg.cols, msg.rows, projectId);
|
|
449
451
|
}
|
|
450
452
|
else if (type === "close") {
|
|
451
453
|
// Unsubscribe this client only — TerminalManager is shared across
|
|
452
454
|
// all mux connections so we must not kill the PTY here.
|
|
453
|
-
const unsub = subscriptions.get(
|
|
455
|
+
const unsub = subscriptions.get(subscriptionKey);
|
|
454
456
|
if (unsub) {
|
|
455
457
|
unsub();
|
|
456
|
-
subscriptions.delete(
|
|
458
|
+
subscriptions.delete(subscriptionKey);
|
|
457
459
|
}
|
|
458
460
|
}
|
|
459
461
|
}
|
|
@@ -464,6 +466,7 @@ export function createMuxWebSocket(tmuxPath) {
|
|
|
464
466
|
id,
|
|
465
467
|
type: "error",
|
|
466
468
|
message: err instanceof Error ? err.message : String(err),
|
|
469
|
+
...(projectId && { projectId }),
|
|
467
470
|
};
|
|
468
471
|
ws.send(JSON.stringify(errorMsg));
|
|
469
472
|
}
|
|
@@ -472,10 +475,19 @@ export function createMuxWebSocket(tmuxPath) {
|
|
|
472
475
|
else if (msg.ch === "subscribe") {
|
|
473
476
|
if (msg.topics.includes("sessions") && !sessionUnsubscribe) {
|
|
474
477
|
sessionUnsubscribe = broadcaster.subscribe((sessions) => {
|
|
475
|
-
if (ws.readyState
|
|
476
|
-
|
|
477
|
-
|
|
478
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
479
|
+
return;
|
|
480
|
+
if (ws.bufferedAmount > WS_BUFFER_HIGH_WATERMARK) {
|
|
481
|
+
console.warn("[MuxServer] Skipping session snapshot — socket backpressured");
|
|
482
|
+
return;
|
|
478
483
|
}
|
|
484
|
+
const snapMsg = { ch: "sessions", type: "snapshot", sessions };
|
|
485
|
+
ws.send(JSON.stringify(snapMsg));
|
|
486
|
+
}, (error) => {
|
|
487
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
488
|
+
return;
|
|
489
|
+
const errMsg = { ch: "sessions", type: "error", error };
|
|
490
|
+
ws.send(JSON.stringify(errMsg));
|
|
479
491
|
});
|
|
480
492
|
}
|
|
481
493
|
}
|
|
@@ -5,10 +5,74 @@
|
|
|
5
5
|
* so the logic can be properly unit tested.
|
|
6
6
|
*/
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
8
11
|
/** Session ID validation regex — alphanumeric, hyphens, underscores only */
|
|
9
12
|
export const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
10
13
|
/** Hash prefix pattern — 12-char lowercase hex, as generated by generateConfigHash */
|
|
11
14
|
const HASH_PREFIX_PATTERN = /^[a-f0-9]{12}-/;
|
|
15
|
+
/**
|
|
16
|
+
* StorageKey pattern — either bare 12-hex hash or `{hash}-{projectName}`
|
|
17
|
+
* wrapped form. The wrapped suffix comes from `basename(projectPath)` on
|
|
18
|
+
* disk so it can contain any character a filesystem path component allows
|
|
19
|
+
* (spaces, unicode, etc.); we only require that something non-empty
|
|
20
|
+
* follows the `-` separator. Security: storageKey is passed to
|
|
21
|
+
* `execFileSync` which bypasses the shell, so arbitrary characters are
|
|
22
|
+
* safe in the argv.
|
|
23
|
+
*/
|
|
24
|
+
const STORAGE_KEY_PATTERN = /^[a-f0-9]{12}(-.+)?$/;
|
|
25
|
+
const defaultFs = {
|
|
26
|
+
// Only return subdirectory names. `readdirSync` without withFileTypes
|
|
27
|
+
// includes plain files, so a stray file like `aabbccddeef0` would pass
|
|
28
|
+
// STORAGE_KEY_PATTERN and trigger an unnecessary existsSync probe.
|
|
29
|
+
readdir: (p) => readdirSync(p, { withFileTypes: true })
|
|
30
|
+
.filter((e) => e.isDirectory())
|
|
31
|
+
.map((e) => e.name),
|
|
32
|
+
exists: (p) => existsSync(p),
|
|
33
|
+
homedir,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Find every storageKey that owns a given sessionId by scanning the AO
|
|
37
|
+
* base directory for projects whose `sessions/{sessionId}` file exists.
|
|
38
|
+
*
|
|
39
|
+
* This is the authoritative disambiguation step: tmux names alone are
|
|
40
|
+
* ambiguous when storageKey can take either the bare-hash or wrapped
|
|
41
|
+
* `{hash}-{projectName}` form. The on-disk session record is unique per
|
|
42
|
+
* storageKey so it tells us exactly which tmux name to expect.
|
|
43
|
+
*
|
|
44
|
+
* Returns all candidates (not just the first). Multiple projects can
|
|
45
|
+
* share a sessionId like `app-1`, and the caller must probe each until
|
|
46
|
+
* it finds a live tmux session — otherwise a stale metadata dir from
|
|
47
|
+
* one project could shadow the live session of another.
|
|
48
|
+
*/
|
|
49
|
+
function findStorageKeysForSession(sessionId, fs, projectId) {
|
|
50
|
+
const aoBase = join(fs.homedir(), ".agent-orchestrator");
|
|
51
|
+
let entries;
|
|
52
|
+
try {
|
|
53
|
+
entries = fs.readdir(aoBase);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const matches = [];
|
|
59
|
+
const projectMatches = [];
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!STORAGE_KEY_PATTERN.test(entry))
|
|
62
|
+
continue;
|
|
63
|
+
const sessionFile = join(aoBase, entry, "sessions", sessionId);
|
|
64
|
+
if (fs.exists(sessionFile)) {
|
|
65
|
+
const unwrappedProjectId = entry.slice(13); // Strip "{hash}-" prefix when present.
|
|
66
|
+
if (projectId && (entry === projectId || unwrappedProjectId === projectId)) {
|
|
67
|
+
projectMatches.push(entry);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
matches.push(entry);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [...projectMatches, ...matches];
|
|
75
|
+
}
|
|
12
76
|
/**
|
|
13
77
|
* Validate a session ID format.
|
|
14
78
|
* Prevents path traversal, shell injection, and other attacks.
|
|
@@ -45,20 +109,29 @@ export function findTmux(execFn = execFileSync) {
|
|
|
45
109
|
/**
|
|
46
110
|
* Resolve a user-facing session ID to its actual tmux session name.
|
|
47
111
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
112
|
+
* ao-core names tmux sessions as `{storageKey}-{sessionId}`, where
|
|
113
|
+
* storageKey is either `{12-hex}` (bare hash) or `{12-hex}-{projectName}`
|
|
114
|
+
* (legacy wrapped format). This function:
|
|
50
115
|
*
|
|
51
|
-
* 1. Tries exact match
|
|
116
|
+
* 1. Tries exact match using tmux's `=` prefix syntax to prevent
|
|
52
117
|
* prefix matching (where "ao-1" would incorrectly match "ao-15").
|
|
53
|
-
* 2.
|
|
54
|
-
*
|
|
118
|
+
* 2. Looks up the storageKey owning this sessionId on disk (under
|
|
119
|
+
* `~/.agent-orchestrator/{storageKey}/sessions/{sessionId}`) and asks
|
|
120
|
+
* tmux whether the exact `{storageKey}-{sessionId}` session exists.
|
|
121
|
+
* The on-disk check is authoritative — it avoids ambiguous suffix
|
|
122
|
+
* matches where a bare session like `{hash}-my-app-1` could be
|
|
123
|
+
* mistaken for a lookup of `app-1`.
|
|
124
|
+
* 3. Falls back to listing sessions and matching a hash-prefixed name
|
|
125
|
+
* whose remainder equals the sessionId (bare-hash only), so behavior
|
|
126
|
+
* stays correct even if the on-disk session record is absent.
|
|
55
127
|
*
|
|
56
128
|
* @param sessionId - User-facing session ID (e.g., "ao-15")
|
|
57
129
|
* @param tmuxPath - Full path to tmux binary
|
|
58
130
|
* @param execFn - Injectable execFileSync for testing. Defaults to child_process.execFileSync.
|
|
131
|
+
* @param fs - Injectable filesystem adapter for testing.
|
|
59
132
|
* @returns The actual tmux session name, or null if not found
|
|
60
133
|
*/
|
|
61
|
-
export function resolveTmuxSession(sessionId, tmuxPath, execFn = execFileSync) {
|
|
134
|
+
export function resolveTmuxSession(sessionId, tmuxPath, execFn = execFileSync, fs = defaultFs, projectId) {
|
|
62
135
|
// Try exact match first using = prefix for exact matching (e.g., "ao-orchestrator")
|
|
63
136
|
// Without =, tmux uses prefix matching: "ao-1" would match "ao-15"
|
|
64
137
|
try {
|
|
@@ -68,9 +141,26 @@ export function resolveTmuxSession(sessionId, tmuxPath, execFn = execFileSync) {
|
|
|
68
141
|
catch {
|
|
69
142
|
// Not an exact match
|
|
70
143
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
144
|
+
// Authoritative path: find candidate storageKeys on disk, then verify
|
|
145
|
+
// each exact tmux session name with has-session. This is unambiguous
|
|
146
|
+
// even when the storageKey is wrapped (`{hash}-{projectName}`). Walk
|
|
147
|
+
// every candidate so a stale metadata dir in one project can't shadow
|
|
148
|
+
// the live session of another project with the same sessionId.
|
|
149
|
+
for (const storageKey of findStorageKeysForSession(sessionId, fs, projectId)) {
|
|
150
|
+
const tmuxName = `${storageKey}-${sessionId}`;
|
|
151
|
+
try {
|
|
152
|
+
execFn(tmuxPath, ["has-session", "-t", `=${tmuxName}`], { timeout: 5000 });
|
|
153
|
+
return tmuxName;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Session dir exists but tmux session doesn't — try next candidate
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Fallback: list sessions and match the bare-hash form only. We
|
|
160
|
+
// intentionally do NOT match by trailing suffix here — that would cause
|
|
161
|
+
// `app-1` to falsely resolve a distinct session `{hash}-my-app-1`. If a
|
|
162
|
+
// wrapped-storageKey session isn't findable on disk above, it's safer
|
|
163
|
+
// to return null than to guess.
|
|
74
164
|
try {
|
|
75
165
|
const output = execFn(tmuxPath, ["list-sessions", "-F", "#{session_name}"], {
|
|
76
166
|
timeout: 5000,
|
package/next.config.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
|
|
1
6
|
/** @type {import('next').NextConfig} */
|
|
2
7
|
const nextConfig = {
|
|
8
|
+
outputFileTracingRoot: path.join(__dirname, "../.."),
|
|
3
9
|
transpilePackages: [
|
|
4
10
|
"@aoagents/ao-core",
|
|
5
11
|
"@aoagents/ao-plugin-agent-claude-code",
|