From 372d74487930862f2cdcf668613c2fc1f098a0c7 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 18 Apr 2026 20:14:49 +0300 Subject: [PATCH] refactor(team): store runtime identity structurally --- package.json | 2 + pnpm-lock.yaml | 101 ++-- .../main/composition/runtimeSupport.ts | 6 + src/features/tmux-installer/main/index.ts | 1 + .../runtime/TmuxPlatformCommandExecutor.ts | 30 ++ .../TmuxPlatformCommandExecutor.test.ts | 22 + .../services/team/TeamProvisioningService.ts | 486 +++++++++++++----- .../team/TeamProvisioningService.test.ts | 121 +++++ 8 files changed, 603 insertions(+), 166 deletions(-) diff --git a/package.json b/package.json index afeebaa0..9809f68c 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "motion": "12.38.0", "node-diff3": "^3.2.0", "node-pty": "^1.1.0", + "pidusage": "4.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-grid-layout": "^2.2.2", @@ -174,6 +175,7 @@ "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "@types/node": "^25.0.7", + "@types/pidusage": "2.0.5", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/ssh2": "^1.15.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66dc8138..e5b3e08f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -239,6 +239,9 @@ importers: node-pty: specifier: ^1.1.0 version: 1.1.0 + pidusage: + specifier: 4.0.1 + version: 4.0.1 react: specifier: ^19.0.0 version: 19.2.4 @@ -327,6 +330,9 @@ importers: '@types/node': specifier: ^25.0.7 version: 25.0.7 + '@types/pidusage': + specifier: 2.0.5 + version: 2.0.5 '@types/react': specifier: ^19.0.0 version: 19.2.14 @@ -451,13 +457,13 @@ importers: version: 0.11.3(magicast@0.5.2)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))) '@vueuse/nuxt': specifier: ^10.11.1 - version: 10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)) + version: 10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)) nuxt: specifier: ^3.20.2 - version: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + version: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2) nuxt-icon: specifier: ^0.6.10 - version: 0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)) + version: 0.6.10(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3)) pinia: specifier: ^3.0.4 version: 3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)) @@ -482,7 +488,7 @@ importers: version: 7.4.47 '@nuxt/eslint': specifier: ^1.12.1 - version: 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)) eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) @@ -494,7 +500,7 @@ importers: version: 1.98.0 vite-plugin-vuetify: specifier: ^2.1.3 - version: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3) + version: 2.1.3(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3) mcp-server: dependencies: @@ -4353,6 +4359,9 @@ packages: '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/pidusage@2.0.5': + resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==} + '@types/plist@3.0.5': resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} @@ -8591,6 +8600,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + pidusage@4.0.1: + resolution: {integrity: sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==} + engines: {node: '>=18'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -12589,20 +12602,20 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@nuxt/devtools-kit@1.7.0(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))': dependencies: '@nuxt/kit': 3.21.2(magicast@0.5.2) '@nuxt/schema': 3.21.2 execa: 7.2.0 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0) transitivePeerDependencies: - magicast - '@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))': dependencies: '@nuxt/kit': 4.4.2(magicast@0.5.2) execa: 8.0.1 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0) transitivePeerDependencies: - magicast @@ -12617,9 +12630,9 @@ snapshots: pkg-types: 2.3.0 semver: 7.7.4 - '@nuxt/devtools@3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))': + '@nuxt/devtools@3.2.4(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))': dependencies: - '@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)) '@nuxt/devtools-wizard': 3.2.4 '@nuxt/kit': 4.4.2(magicast@0.5.2) '@vue/devtools-core': 8.1.0(vue@3.5.30(typescript@5.9.3)) @@ -12647,9 +12660,9 @@ snapshots: sirv: 3.0.2 structured-clone-es: 2.0.0 tinyglobby: 0.2.15 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - vite-plugin-vue-tracer: 1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)) + vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0) + vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)) + vite-plugin-vue-tracer: 1.3.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3)) which: 6.0.1 ws: 8.20.0 transitivePeerDependencies: @@ -12698,10 +12711,10 @@ snapshots: - supports-color - typescript - '@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))': dependencies: '@eslint/config-inspector': 1.5.0(eslint@9.39.2(jiti@2.6.1)) - '@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)) '@nuxt/eslint-config': 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@nuxt/eslint-plugin': 1.15.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@nuxt/kit': 4.4.2(magicast@0.5.2) @@ -12777,7 +12790,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)': + '@nuxt/nitro-server@3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(typescript@5.9.3)': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 3.21.2(magicast@0.5.2) @@ -12795,7 +12808,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.13.2(encoding@0.1.13)(idb-keyval@6.2.2) - nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2) ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.0 @@ -12860,7 +12873,7 @@ snapshots: rc9: 3.0.0 std-env: 3.10.0 - '@nuxt/vite-builder@3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)': + '@nuxt/vite-builder@3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)': dependencies: '@nuxt/kit': 3.21.2(magicast@0.5.2) '@rollup/plugin-replace': 6.0.3(rollup@4.60.0) @@ -12879,7 +12892,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2) nypm: 0.6.5 ohash: 2.0.11 pathe: 2.0.3 @@ -15002,6 +15015,8 @@ snapshots: pg-protocol: 1.13.0 pg-types: 2.2.0 + '@types/pidusage@2.0.5': {} + '@types/plist@3.0.5': dependencies: '@types/node': 25.0.7 @@ -15595,13 +15610,13 @@ snapshots: '@vueuse/metadata@10.11.1': {} - '@vueuse/nuxt@10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))': + '@vueuse/nuxt@10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))': dependencies: '@nuxt/kit': 3.21.2(magicast@0.5.2) '@vueuse/core': 10.11.1(vue@3.5.30(typescript@5.9.3)) '@vueuse/metadata': 10.11.1 local-pkg: 0.5.1 - nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2) vue-demi: 0.14.10(vue@3.5.30(typescript@5.9.3)) transitivePeerDependencies: - '@vue/composition-api' @@ -20143,27 +20158,27 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt-icon@0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)): + nuxt-icon@0.6.10(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3)): dependencies: '@iconify/collections': 1.0.665 '@iconify/vue': 4.3.0(vue@3.5.30(typescript@5.9.3)) - '@nuxt/devtools-kit': 1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)) '@nuxt/kit': 3.21.2(magicast@0.5.2) transitivePeerDependencies: - magicast - vite - vue - nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2): + nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2): dependencies: '@dxup/nuxt': 0.4.0(magicast@0.5.2)(typescript@5.9.3) '@nuxt/cli': 3.34.0(@nuxt/schema@3.21.2)(cac@6.7.14)(magicast@0.5.2) - '@nuxt/devtools': 3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)) + '@nuxt/devtools': 3.2.4(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3)) '@nuxt/kit': 3.21.2(magicast@0.5.2) - '@nuxt/nitro-server': 3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3) + '@nuxt/nitro-server': 3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(typescript@5.9.3) '@nuxt/schema': 3.21.2 '@nuxt/telemetry': 2.7.0(@nuxt/kit@3.21.2(magicast@0.5.2)) - '@nuxt/vite-builder': 3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2) + '@nuxt/vite-builder': 3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2) '@unhead/vue': 2.1.12(vue@3.5.30(typescript@5.9.3)) '@vue/shared': 3.5.30 c12: 3.3.3(magicast@0.5.2) @@ -20647,6 +20662,10 @@ snapshots: pidtree@0.6.0: {} + pidusage@4.0.1: + dependencies: + safe-buffer: 5.2.1 + pify@2.3.0: {} pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)): @@ -22710,15 +22729,15 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-dev-rpc@1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vite-dev-rpc@1.1.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)): dependencies: birpc: 2.9.0 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vite-hot-client: 2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0) + vite-hot-client: 2.1.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)) - vite-hot-client@2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vite-hot-client@2.1.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)): dependencies: - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0) vite-node@3.2.4(@types/node@22.19.15)(sass@1.98.0)(terser@5.46.0): dependencies: @@ -22792,7 +22811,7 @@ snapshots: optionator: 0.9.4 typescript: 5.9.3 - vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)): dependencies: ansis: 4.2.0 debug: 4.4.3 @@ -22802,29 +22821,29 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vite-dev-rpc: 1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0) + vite-dev-rpc: 1.1.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)) optionalDependencies: '@nuxt/kit': 4.4.2(magicast@0.5.2) transitivePeerDependencies: - supports-color - vite-plugin-vue-tracer@1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)): + vite-plugin-vue-tracer@1.3.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3)): dependencies: estree-walker: 3.0.3 exsolve: 1.0.8 magic-string: 0.30.21 pathe: 2.0.3 source-map-js: 1.2.1 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0) vue: 3.5.30(typescript@5.9.3) - vite-plugin-vuetify@2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3): + vite-plugin-vuetify@2.1.3(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3): dependencies: '@vuetify/loader-shared': 2.1.2(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3) debug: 4.4.3 upath: 2.0.1 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0) vue: 3.5.30(typescript@5.9.3) vuetify: 3.12.3(typescript@5.9.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.30(typescript@5.9.3)) transitivePeerDependencies: @@ -23023,7 +23042,7 @@ snapshots: vue: 3.5.30(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 - vite-plugin-vuetify: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3) + vite-plugin-vuetify: 2.1.3(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3) w3c-keyname@2.2.8: {} diff --git a/src/features/tmux-installer/main/composition/runtimeSupport.ts b/src/features/tmux-installer/main/composition/runtimeSupport.ts index ee6c41dc..183b918b 100644 --- a/src/features/tmux-installer/main/composition/runtimeSupport.ts +++ b/src/features/tmux-installer/main/composition/runtimeSupport.ts @@ -18,6 +18,12 @@ export async function killTmuxPaneForCurrentPlatform(paneId: string): Promise> { + return runtimeCommandExecutor.listPanePids(paneIds); +} + export function killTmuxPaneForCurrentPlatformSync(paneId: string): void { runtimeCommandExecutor.killPaneSync(paneId); invalidateTmuxRuntimeStatusCache(); diff --git a/src/features/tmux-installer/main/index.ts b/src/features/tmux-installer/main/index.ts index deddfc10..21c41e9f 100644 --- a/src/features/tmux-installer/main/index.ts +++ b/src/features/tmux-installer/main/index.ts @@ -9,4 +9,5 @@ export { isTmuxRuntimeReadyForCurrentPlatform, killTmuxPaneForCurrentPlatform, killTmuxPaneForCurrentPlatformSync, + listTmuxPanePidsForCurrentPlatform, } from './composition/runtimeSupport'; diff --git a/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts b/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts index 948c86ba..4b062134 100644 --- a/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts +++ b/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts @@ -54,6 +54,36 @@ export class TmuxPlatformCommandExecutor { } } + async listPanePids(paneIds: readonly string[]): Promise> { + const normalizedPaneIds = [...new Set(paneIds.map((paneId) => paneId.trim()).filter(Boolean))]; + if (normalizedPaneIds.length === 0) { + return new Map(); + } + + const result = await this.execTmux( + ['list-panes', '-a', '-F', '#{pane_id}\t#{pane_pid}'], + 3_000 + ); + if (result.exitCode !== 0) { + throw new Error(result.stderr || 'Failed to list tmux panes'); + } + + const wanted = new Set(normalizedPaneIds); + const panePidById = new Map(); + for (const line of result.stdout.split('\n')) { + const trimmed = line.trim(); + if (!trimmed) continue; + const [paneId = '', rawPid = ''] = trimmed.split('\t'); + const normalizedPaneId = paneId.trim(); + if (!wanted.has(normalizedPaneId)) continue; + const pid = Number.parseInt(rawPid.trim(), 10); + if (Number.isFinite(pid) && pid > 0) { + panePidById.set(normalizedPaneId, pid); + } + } + return panePidById; + } + killPaneSync(paneId: string): void { if (process.platform === 'win32') { const preferredDistro = this.#wslService.getPersistedPreferredDistroSync(); diff --git a/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts b/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts index 314fdbdc..376afc79 100644 --- a/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts +++ b/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts @@ -68,4 +68,26 @@ describe('TmuxPlatformCommandExecutor', () => { }) ); }); + + it('lists pane pids for the requested pane ids only', async () => { + const executor = new TmuxPlatformCommandExecutor( + { + getPersistedPreferredDistroSync: () => null, + } as never, + {} as never + ); + vi.spyOn(executor, 'execTmux').mockResolvedValue({ + exitCode: 0, + stdout: '%1\t111\n%2\t222\n%3\tnot-a-pid\n', + stderr: '', + }); + + await expect(executor.listPanePids(['%2', '%3', '%2'])).resolves.toEqual( + new Map([['%2', 222]]) + ); + expect(executor.execTmux).toHaveBeenCalledWith( + ['list-panes', '-a', '-F', '#{pane_id}\t#{pane_pid}'], + 3_000 + ); + }); }); diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index f3fb5fb8..80a74ba0 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -1,4 +1,7 @@ -import { killTmuxPaneForCurrentPlatformSync } from '@features/tmux-installer/main'; +import { + killTmuxPaneForCurrentPlatformSync, + listTmuxPanePidsForCurrentPlatform, +} from '@features/tmux-installer/main'; import { ConfigManager } from '@main/services/infrastructure/ConfigManager'; import { NotificationManager } from '@main/services/infrastructure/NotificationManager'; import { getAppIconPath } from '@main/utils/appIcon'; @@ -65,6 +68,7 @@ import { randomUUID } from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import pidusage from 'pidusage'; import { type GeminiRuntimeAuthState, @@ -168,13 +172,13 @@ import type { MemberSpawnStatusEntry, PersistedTeamLaunchPhase, PersistedTeamLaunchSummary, + TeamAgentRuntimeBackendType, + TeamAgentRuntimeEntry, + TeamAgentRuntimeSnapshot, TeamChangeEvent, TeamConfig, TeamCreateRequest, TeamCreateResponse, - TeamAgentRuntimeBackendType, - TeamAgentRuntimeEntry, - TeamAgentRuntimeSnapshot, TeamLaunchAggregateState, TeamLaunchRequest, TeamLaunchResponse, @@ -794,9 +798,38 @@ function createInitialMemberSpawnStatusEntry(): MemberSpawnStatusEntry { } interface LiveTeamAgentRuntimeMetadata { + alive: boolean; + backendType?: TeamAgentRuntimeBackendType; + agentId?: string; pid?: number; model?: string; - rssBytes?: number; + tmuxPaneId?: string; +} + +function normalizeTeamAgentRuntimeBackendType( + value: string | undefined, + isLead: boolean +): TeamAgentRuntimeBackendType | undefined { + if (isLead) return 'lead'; + const normalized = value?.trim().toLowerCase(); + if (normalized === 'tmux' || normalized === 'iterm2' || normalized === 'in-process') { + return normalized; + } + return normalized ? 'process' : undefined; +} + +function matchesMemberNameOrBase(candidateName: string, memberName: string): boolean { + if (candidateName === memberName) { + return true; + } + const parsed = parseNumericSuffixName(candidateName); + return parsed !== null && parsed.suffix >= 2 && parsed.base === memberName; +} + +function matchesTeamMemberIdentity(leftName: string, rightName: string): boolean { + return ( + matchesMemberNameOrBase(leftName, rightName) || matchesMemberNameOrBase(rightName, leftName) + ); } function stripWrappedCliFlagValue(raw: string | undefined): string | undefined { @@ -2180,17 +2213,17 @@ interface McpJsonRpcResponse { } interface McpToolsListResult { - tools?: Array<{ + tools?: { name?: string; _meta?: Record; - }>; + }[]; } interface McpToolCallResult { - content?: Array<{ + content?: { type?: string; text?: string; - }>; + }[]; isError?: boolean; } @@ -2382,6 +2415,10 @@ export class TeamProvisioningService { string, { expiresAtMs: number; snapshot: TeamAgentRuntimeSnapshot } >(); + private readonly liveTeamAgentRuntimeMetadataCache = new Map< + string, + { expiresAtMs: number; metadata: Map } + >(); private readonly launchStateStore = new TeamLaunchStateStore(); private readonly memberLogsFinder: TeamMemberLogsFinder; private teamChangeEmitter: ((event: TeamChangeEvent) => void) | null = null; @@ -3759,17 +3796,18 @@ export class TeamProvisioningService { const runId = this.getTrackedRunId(teamName); if (!runId) { return this.reconcilePersistedLaunchState(teamName).then(({ snapshot, statuses }) => { - this.attachLiveRuntimeMetadataToStatuses(teamName, statuses); - return { - statuses, - runId: null, - teamLaunchState: snapshot?.teamLaunchState, - launchPhase: snapshot?.launchPhase, - expectedMembers: snapshot?.expectedMembers, - updatedAt: snapshot?.updatedAt, - summary: snapshot?.summary, - source: snapshot ? 'persisted' : 'persisted', - }; + return this.attachLiveRuntimeMetadataToStatuses(teamName, statuses).then( + (nextStatuses) => ({ + statuses: nextStatuses, + runId: null, + teamLaunchState: snapshot?.teamLaunchState, + launchPhase: snapshot?.launchPhase, + expectedMembers: snapshot?.expectedMembers, + updatedAt: snapshot?.updatedAt, + summary: snapshot?.summary, + source: snapshot ? 'persisted' : 'persisted', + }) + ); }); } const run = this.runs.get(runId); @@ -3790,8 +3828,10 @@ export class TeamProvisioningService { statuses: this.buildRuntimeSpawnStatusRecord(run), }); const snapshot = persisted ?? liveSnapshot; - const statuses = snapshotToMemberSpawnStatuses(snapshot); - this.attachLiveRuntimeMetadataToStatuses(teamName, statuses); + const statuses = await this.attachLiveRuntimeMetadataToStatuses( + teamName, + snapshotToMemberSpawnStatuses(snapshot) + ); return { statuses, runId, @@ -3821,8 +3861,19 @@ export class TeamProvisioningService { configuredMembers = []; } - const unixProcessRows = this.readUnixProcessTableRows(); - const liveRuntimeByMember = this.getLiveTeamAgentRuntimeMetadata(teamName, unixProcessRows); + const liveRuntimeByMember = await this.getLiveTeamAgentRuntimeMetadata(teamName); + const runtimePids = new Set(); + const leadPid = run?.child?.pid; + if (typeof leadPid === 'number' && Number.isFinite(leadPid) && leadPid > 0) { + runtimePids.add(leadPid); + } + for (const metadata of liveRuntimeByMember.values()) { + const memberPid = metadata.pid; + if (typeof memberPid === 'number' && Number.isFinite(memberPid) && memberPid > 0) { + runtimePids.add(memberPid); + } + } + const rssBytesByPid = await this.readProcessRssBytesByPid([...runtimePids]); const persistedRuntimeMembers = this.readPersistedRuntimeMembers(teamName); const snapshotMembers: Record = {}; @@ -3831,10 +3882,7 @@ export class TeamProvisioningService { ): PersistedRuntimeMemberLike | undefined => { return persistedRuntimeMembers.find((member) => { const candidateName = typeof member.name === 'string' ? member.name.trim() : ''; - if (!candidateName) return false; - if (candidateName === memberName) return true; - const parsed = parseNumericSuffixName(candidateName); - return parsed !== null && parsed.suffix >= 2 && parsed.base === memberName; + return candidateName.length > 0 && matchesMemberNameOrBase(candidateName, memberName); }); }; @@ -3844,26 +3892,13 @@ export class TeamProvisioningService { if (candidateName === memberName) { return metadata; } - const parsed = parseNumericSuffixName(candidateName); - if (parsed !== null && parsed.suffix >= 2 && parsed.base === memberName) { + if (matchesMemberNameOrBase(candidateName, memberName)) { fallback = metadata; } } return fallback; }; - const normalizeBackendType = ( - value: string | undefined, - isLead: boolean - ): TeamAgentRuntimeBackendType | undefined => { - if (isLead) return 'lead'; - const normalized = value?.trim().toLowerCase(); - if (normalized === 'tmux' || normalized === 'iterm2' || normalized === 'in-process') { - return normalized; - } - return normalized ? 'process' : undefined; - }; - for (const member of configuredMembers) { const memberName = typeof member?.name === 'string' ? member.name.trim() : ''; if (!memberName) continue; @@ -3871,7 +3906,7 @@ export class TeamProvisioningService { const isLead = isLeadMember({ name: memberName, agentType: member.agentType }); if (isLead) { const pid = run?.child?.pid; - const rssBytes = pid ? this.lookupProcessRssBytes(pid, unixProcessRows) : undefined; + const rssBytes = pid ? rssBytesByPid.get(pid) : undefined; const runtimeModel = run?.request.model?.trim() || (run?.spawnContext @@ -3894,18 +3929,23 @@ export class TeamProvisioningService { const persistedRuntimeMember = getPersistedRuntimeMember(memberName); const liveRuntimeMember = getLiveRuntimeMember(memberName); - const backendType = normalizeBackendType(persistedRuntimeMember?.backendType, false); + const backendType = normalizeTeamAgentRuntimeBackendType( + persistedRuntimeMember?.backendType, + false + ); const restartable = backendType !== 'in-process'; const runtimeModel = liveRuntimeMember?.model ?? member.model?.trim() ?? undefined; snapshotMembers[memberName] = { memberName, - alive: Boolean(liveRuntimeMember?.pid), + alive: liveRuntimeMember?.alive ?? false, restartable, ...(backendType ? { backendType } : {}), ...(liveRuntimeMember?.pid ? { pid: liveRuntimeMember.pid } : {}), ...(runtimeModel ? { runtimeModel } : {}), - ...(liveRuntimeMember?.rssBytes != null ? { rssBytes: liveRuntimeMember.rssBytes } : {}), + ...(liveRuntimeMember?.pid && rssBytesByPid.has(liveRuntimeMember.pid) + ? { rssBytes: rssBytesByPid.get(liveRuntimeMember.pid) } + : {}), updatedAt, }; } @@ -3951,10 +3991,7 @@ export class TeamProvisioningService { const persistedRuntimeMembers = this.readPersistedRuntimeMembers(teamName).filter((member) => { const candidateName = typeof member.name === 'string' ? member.name.trim() : ''; - if (!candidateName) return false; - if (candidateName === memberName) return true; - const parsed = parseNumericSuffixName(candidateName); - return parsed !== null && parsed.suffix >= 2 && parsed.base === memberName; + return candidateName.length > 0 && matchesMemberNameOrBase(candidateName, memberName); }); const backendTypes = new Set( @@ -3968,18 +4005,26 @@ export class TeamProvisioningService { ); } - const unixProcessRows = this.readUnixProcessTableRows(); - const liveRuntimeByMember = this.getLiveTeamAgentRuntimeMetadata(teamName, unixProcessRows); + const liveRuntimeByMember = await this.getLiveTeamAgentRuntimeMetadata(teamName); const livePids = new Set(); + let hasAliveRuntimeWithoutPid = false; for (const [candidateName, metadata] of liveRuntimeByMember.entries()) { - if (candidateName === memberName) { - if (metadata.pid) livePids.add(metadata.pid); + if (!matchesMemberNameOrBase(candidateName, memberName)) { continue; } - const parsed = parseNumericSuffixName(candidateName); - if (parsed !== null && parsed.suffix >= 2 && parsed.base === memberName && metadata.pid) { + if (metadata.pid) { livePids.add(metadata.pid); + continue; } + if (metadata.alive && metadata.backendType !== 'in-process') { + hasAliveRuntimeWithoutPid = true; + } + } + + if (hasAliveRuntimeWithoutPid) { + throw new Error( + `Member "${memberName}" is running, but its backend does not expose a restartable pid yet` + ); } for (const persistedRuntimeMember of persistedRuntimeMembers) { @@ -4025,6 +4070,7 @@ export class TeamProvisioningService { } this.agentRuntimeSnapshotCache.delete(teamName); + this.liveTeamAgentRuntimeMetadataCache.delete(teamName); this.setMemberSpawnStatus(run, memberName, 'offline'); this.setMemberSpawnStatus(run, memberName, 'spawning'); this.appendMemberBootstrapDiagnostic(run, memberName, 'manual restart requested from UI'); @@ -7471,7 +7517,7 @@ export class TeamProvisioningService { return; } - const liveAgentNames = this.getLiveTeamAgentNames(run.teamName); + const liveAgentNames = await this.getLiveTeamAgentNames(run.teamName); // Flag any expected member not found in config.json (excluding the lead) for (const expected of run.expectedMembers) { @@ -7581,42 +7627,245 @@ export class TeamProvisioningService { } } - private hasLiveTeamAgentProcess(teamName: string, memberName: string): boolean { - return this.getLiveTeamAgentRuntimeMetadata(teamName).has(memberName); - } - - private attachLiveRuntimeMetadataToStatuses( + private async attachLiveRuntimeMetadataToStatuses( teamName: string, statuses: Record - ): void { - for (const [memberName, metadata] of this.getLiveTeamAgentRuntimeMetadata(teamName).entries()) { - const current = statuses[memberName]; + ): Promise> { + const runtimeByMember = await this.getLiveTeamAgentRuntimeMetadata(teamName); + const nextStatuses = { ...statuses }; + for (const [memberName, metadata] of runtimeByMember.entries()) { + const current = nextStatuses[memberName]; if (!current || !metadata.model) { continue; } - statuses[memberName] = { + nextStatuses[memberName] = { ...current, runtimeModel: metadata.model, }; } + return nextStatuses; } - private getLiveTeamAgentNames(teamName: string): Set { - return new Set(this.getLiveTeamAgentRuntimeMetadata(teamName).keys()); + private async getLiveTeamAgentNames(teamName: string): Promise> { + const runtimeByMember = await this.getLiveTeamAgentRuntimeMetadata(teamName); + return new Set( + [...runtimeByMember.entries()] + .filter(([, metadata]) => metadata.alive) + .map(([memberName]) => memberName) + ); } - private readUnixProcessTableRows(): Array<{ + private findConfiguredMemberModel( + configuredMembers: TeamConfig['members'] | undefined, + memberName: string + ): string | undefined { + for (const member of configuredMembers ?? []) { + const candidateName = typeof member?.name === 'string' ? member.name.trim() : ''; + if (!candidateName || !matchesTeamMemberIdentity(candidateName, memberName)) { + continue; + } + const model = member.model?.trim(); + if (model) { + return model; + } + } + return undefined; + } + + private findMetaMemberModel( + metaMembers: Awaited>, + memberName: string + ): string | undefined { + for (const member of metaMembers) { + const candidateName = member.name?.trim() ?? ''; + if (!candidateName || !matchesTeamMemberIdentity(candidateName, memberName)) { + continue; + } + const model = member.model?.trim(); + if (model) { + return model; + } + } + return undefined; + } + + private findEffectiveRunMemberModel( + run: ProvisioningRun | null, + memberName: string + ): string | undefined { + if (!run) { + return undefined; + } + for (const member of run.effectiveMembers ?? []) { + const candidateName = member.name?.trim() ?? ''; + if (!candidateName || !matchesTeamMemberIdentity(candidateName, memberName)) { + continue; + } + const model = member.model?.trim(); + if (model) { + return model; + } + } + return undefined; + } + + private findTrackedMemberSpawnStatus( + run: ProvisioningRun | null, + memberName: string + ): MemberSpawnStatusEntry | undefined { + if (!run) { + return undefined; + } + const statusMap = run.memberSpawnStatuses instanceof Map ? run.memberSpawnStatuses : undefined; + if (!statusMap) { + return undefined; + } + const direct = statusMap.get(memberName); + if (direct) { + return direct; + } + for (const [candidateName, entry] of statusMap.entries()) { + if (matchesTeamMemberIdentity(candidateName, memberName)) { + return entry; + } + } + return undefined; + } + + private async getLiveTeamAgentRuntimeMetadata( + teamName: string + ): Promise> { + const cached = this.liveTeamAgentRuntimeMetadataCache.get(teamName); + if (cached && cached.expiresAtMs > Date.now()) { + return cached.metadata; + } + + const runId = this.getTrackedRunId(teamName); + const run = runId ? (this.runs.get(runId) ?? null) : null; + + let configuredMembers: TeamConfig['members'] = []; + try { + configuredMembers = (await this.configReader.getConfig(teamName))?.members ?? []; + } catch { + configuredMembers = []; + } + + let metaMembers: Awaited> = []; + try { + metaMembers = await this.membersMetaStore.getMembers(teamName); + } catch { + metaMembers = []; + } + + const persistedRuntimeMembers = this.readPersistedRuntimeMembers(teamName); + const metadataByMember = new Map(); + const upsertMetadata = ( + memberName: string, + patch: Partial + ): void => { + const current = metadataByMember.get(memberName) ?? { alive: false }; + metadataByMember.set(memberName, { + ...current, + ...patch, + alive: patch.alive ?? current.alive, + }); + }; + + for (const member of persistedRuntimeMembers) { + const memberName = typeof member.name === 'string' ? member.name.trim() : ''; + if (!memberName || isLeadMember({ name: memberName })) { + continue; + } + const runtimeModel = + this.findConfiguredMemberModel(configuredMembers, memberName) ?? + this.findEffectiveRunMemberModel(run, memberName) ?? + this.findMetaMemberModel(metaMembers, memberName); + upsertMetadata(memberName, { + backendType: normalizeTeamAgentRuntimeBackendType(member.backendType, false), + agentId: + typeof member.agentId === 'string' ? member.agentId.trim() || undefined : undefined, + tmuxPaneId: + typeof member.tmuxPaneId === 'string' ? member.tmuxPaneId.trim() || undefined : undefined, + ...(runtimeModel ? { model: runtimeModel } : {}), + }); + } + + for (const member of configuredMembers) { + const memberName = typeof member?.name === 'string' ? member.name.trim() : ''; + if (!memberName || isLeadMember({ name: memberName, agentType: member.agentType })) { + continue; + } + const runtimeModel = + member.model?.trim() || + this.findEffectiveRunMemberModel(run, memberName) || + this.findMetaMemberModel(metaMembers, memberName); + upsertMetadata(memberName, { + ...(runtimeModel ? { model: runtimeModel } : {}), + }); + } + + for (const member of run?.effectiveMembers ?? []) { + const memberName = member.name?.trim() ?? ''; + if (!memberName || isLeadMember(member) || memberName.toLowerCase() === 'user') { + continue; + } + upsertMetadata(memberName, { + ...(member.model?.trim() ? { model: member.model.trim() } : {}), + }); + } + + const paneIds = [...metadataByMember.values()] + .map((metadata) => metadata.tmuxPaneId?.trim() ?? '') + .filter((paneId) => paneId.length > 0); + let panePidById = new Map(); + if (paneIds.length > 0) { + try { + panePidById = await listTmuxPanePidsForCurrentPlatform(paneIds); + } catch (error) { + logger.debug( + `[${teamName}] Failed to read tmux pane pids for runtime snapshot: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + for (const [memberName, metadata] of metadataByMember.entries()) { + const paneId = metadata.tmuxPaneId?.trim() ?? ''; + const backendType = metadata.backendType; + const panePid = paneId ? panePidById.get(paneId) : undefined; + const status = this.findTrackedMemberSpawnStatus(run, memberName); + const alive = + typeof panePid === 'number' && panePid > 0 + ? true + : backendType === 'tmux' + ? false + : Boolean(status?.runtimeAlive || status?.bootstrapConfirmed); + metadataByMember.set(memberName, { + ...metadata, + alive, + ...(typeof panePid === 'number' && panePid > 0 ? { pid: panePid } : {}), + }); + } + + this.liveTeamAgentRuntimeMetadataCache.set(teamName, { + expiresAtMs: Date.now() + TeamProvisioningService.AGENT_RUNTIME_SNAPSHOT_CACHE_TTL_MS, + metadata: metadataByMember, + }); + return metadataByMember; + } + + private readUnixProcessTableRows(): { pid: number; - rssBytes?: number; command: string; - }> { + }[] { if (process.platform === 'win32') { return []; } let output = ''; try { - output = execFileSync('ps', ['-ax', '-o', 'pid=,rss=,command='], { + output = execFileSync('ps', ['-ax', '-o', 'pid=,command='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }); @@ -7624,79 +7873,64 @@ export class TeamProvisioningService { return []; } - const rows: Array<{ pid: number; rssBytes?: number; command: string }> = []; + const rows: { pid: number; command: string }[] = []; for (const line of output.split('\n')) { const trimmed = line.trim(); if (!trimmed) continue; - const match = /^(\d+)\s+(\d+)\s+(.*)$/.exec(trimmed); + const match = /^(\d+)\s+(.*)$/.exec(trimmed); if (!match) continue; const pid = Number.parseInt(match[1], 10); - const rssKb = Number.parseInt(match[2], 10); - const command = match[3]?.trim() ?? ''; + const command = match[2]?.trim() ?? ''; if (!Number.isFinite(pid) || pid <= 0 || command.length === 0) { continue; } rows.push({ pid, - ...(Number.isFinite(rssKb) && rssKb >= 0 ? { rssBytes: rssKb * 1024 } : {}), command, }); } return rows; } - private lookupProcessRssBytes( - pid: number, - unixProcessRows?: Array<{ pid: number; rssBytes?: number; command: string }> - ): number | undefined { - if (!Number.isFinite(pid) || pid <= 0) { - return undefined; - } - - const cached = unixProcessRows?.find((row) => row.pid === pid); - if (cached) { - return cached.rssBytes; - } - - if (process.platform === 'win32') { - return undefined; + private async readProcessRssBytesByPid(pids: readonly number[]): Promise> { + const uniquePids = [...new Set(pids.filter((pid) => Number.isFinite(pid) && pid > 0))]; + if (uniquePids.length === 0) { + return new Map(); } + const rssBytesByPid = new Map(); + const options = { maxage: 0 }; try { - const output = execFileSync('ps', ['-o', 'rss=', '-p', String(pid)], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }); - const rssKb = Number.parseInt(output.trim(), 10); - return Number.isFinite(rssKb) && rssKb >= 0 ? rssKb * 1024 : undefined; - } catch { - return undefined; - } - } - - private getLiveTeamAgentRuntimeMetadata( - teamName: string, - unixProcessRows?: Array<{ pid: number; rssBytes?: number; command: string }> - ): Map { - const teamMarker = `--team-name ${teamName}`; - const metadataByAgent = new Map(); - const rows = unixProcessRows ?? this.readUnixProcessTableRows(); - for (const row of rows) { - const trimmed = row.command.trim(); - if (!trimmed.includes(teamMarker)) continue; - const match = /--agent-id\s+([^\s@]+)@/.exec(trimmed); - if (!match) continue; - const agentName = match[1]?.trim(); - if (agentName) { - const model = extractCliFlagValue(trimmed, '--model'); - metadataByAgent.set(agentName, { - pid: row.pid, - ...(model ? { model } : {}), - ...(row.rssBytes != null ? { rssBytes: row.rssBytes } : {}), - }); + const statsByPid = await pidusage(uniquePids, options); + for (const [rawPid, stat] of Object.entries(statsByPid)) { + const pid = Number.parseInt(rawPid, 10); + const rssBytes = stat?.memory; + if (Number.isFinite(pid) && pid > 0 && Number.isFinite(rssBytes) && rssBytes >= 0) { + rssBytesByPid.set(pid, rssBytes); + } } + return rssBytesByPid; + } catch (error) { + logger.debug( + `pidusage batch runtime snapshot failed; falling back to per-pid reads: ${ + error instanceof Error ? error.message : String(error) + }` + ); } - return metadataByAgent; + + await Promise.all( + uniquePids.map(async (pid) => { + try { + const stat = await pidusage(pid, options); + if (Number.isFinite(stat.memory) && stat.memory >= 0) { + rssBytesByPid.set(pid, stat.memory); + } + } catch { + // Process likely exited between discovery and sampling. + } + }) + ); + return rssBytesByPid; } private async clearPersistedLaunchState(teamName: string): Promise { @@ -7862,7 +8096,7 @@ export class TeamProvisioningService { // best-effort } - const liveAgentNames = this.getLiveTeamAgentNames(teamName); + const liveAgentNames = await this.getLiveTeamAgentNames(teamName); const nextMembers = { ...persisted.members }; const now = nowIso(); for (const expected of persisted.expectedMembers) { @@ -8493,6 +8727,7 @@ export class TeamProvisioningService { */ stopTeam(teamName: string): void { this.agentRuntimeSnapshotCache.delete(teamName); + this.liveTeamAgentRuntimeMetadataCache.delete(teamName); this.stopPersistentTeamMembers(teamName); const runId = this.getTrackedRunId(teamName); @@ -11091,6 +11326,7 @@ export class TeamProvisioningService { } if (!hasNewerTrackedRun) { this.agentRuntimeSnapshotCache.delete(run.teamName); + this.liveTeamAgentRuntimeMetadataCache.delete(run.teamName); this.leadInboxRelayInFlight.delete(run.teamName); this.relayedLeadInboxMessageIds.delete(run.teamName); this.pendingCrossTeamFirstReplies.delete(run.teamName); diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 32753ec5..e7c5f475 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -24,6 +24,19 @@ vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({ ClaudeBinaryResolver: { resolve: vi.fn() }, })); +vi.mock('@features/tmux-installer/main', () => ({ + killTmuxPaneForCurrentPlatformSync: vi.fn(), + listTmuxPanePidsForCurrentPlatform: vi.fn(async () => new Map()), + isTmuxRuntimeReadyForCurrentPlatform: vi.fn(async () => true), +})); + +vi.mock('pidusage', () => { + const pidusageMock = vi.fn(); + return { + default: pidusageMock, + }; +}); + vi.mock('@main/services/team/TeamTaskReader', () => ({ TeamTaskReader: class { async getTasks() { @@ -61,6 +74,8 @@ import { getTeamLaunchStatePath } from '@main/services/team/TeamLaunchStateStore import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver'; import { spawnCli } from '@main/utils/childProcess'; import { AGENT_TEAMS_NAMESPACED_TEAMMATE_OPERATIONAL_TOOL_NAMES } from 'agent-teams-controller'; +import { listTmuxPanePidsForCurrentPlatform } from '@features/tmux-installer/main'; +import pidusage from 'pidusage'; function allowConsoleLogs() { vi.spyOn(console, 'error').mockImplementation(() => {}); @@ -91,6 +106,18 @@ function createRunningChild() { }); } +function createPidusageStat(pid: number, memory: number) { + return { + cpu: 0, + memory, + ppid: 1, + pid, + ctime: 0, + elapsed: 0, + timestamp: Date.now(), + }; +} + function writeLaunchConfig( teamName: string, projectPath: string, @@ -197,6 +224,100 @@ describe('TeamProvisioningService', () => { }); }); + describe('getTeamAgentRuntimeSnapshot', () => { + it('uses batched pidusage rss values for lead and teammates', async () => { + const svc = new TeamProvisioningService(); + (svc as any).configReader = { + getConfig: vi.fn(async () => ({ + members: [ + { name: 'team-lead', agentType: 'team-lead' }, + { name: 'alice', model: 'gpt-5.4-mini' }, + ], + })), + }; + (svc as any).readPersistedRuntimeMembers = vi.fn(() => [ + { + name: 'alice', + agentId: 'alice@runtime-team', + tmuxPaneId: '%1', + backendType: 'tmux', + }, + ]); + (svc as any).aliveRunByTeam.set('runtime-team', 'run-1'); + (svc as any).runs.set('run-1', { + runId: 'run-1', + child: { pid: 111 }, + request: { model: 'gpt-5.4' }, + processKilled: false, + cancelRequested: false, + spawnContext: null, + }); + vi.mocked(listTmuxPanePidsForCurrentPlatform).mockResolvedValueOnce(new Map([['%1', 222]])); + + vi.mocked(pidusage).mockResolvedValueOnce({ + '111': createPidusageStat(111, 123_000_000), + '222': createPidusageStat(222, 456_000_000), + } as any); + + const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team'); + + expect(pidusage).toHaveBeenCalledWith([111, 222], { maxage: 0 }); + expect(snapshot.members['team-lead']).toMatchObject({ + pid: 111, + rssBytes: 123_000_000, + runtimeModel: 'gpt-5.4', + }); + expect(snapshot.members.alice).toMatchObject({ + pid: 222, + rssBytes: 456_000_000, + runtimeModel: 'gpt-5.4-mini', + }); + }); + + it('falls back to per-pid pidusage reads when batched sampling fails', async () => { + const svc = new TeamProvisioningService(); + (svc as any).configReader = { + getConfig: vi.fn(async () => ({ + members: [ + { name: 'team-lead', agentType: 'team-lead' }, + { name: 'alice', model: 'gpt-5.4-mini' }, + ], + })), + }; + (svc as any).readPersistedRuntimeMembers = vi.fn(() => [ + { + name: 'alice', + agentId: 'alice@runtime-team', + tmuxPaneId: '%1', + backendType: 'tmux', + }, + ]); + (svc as any).aliveRunByTeam.set('runtime-team', 'run-1'); + (svc as any).runs.set('run-1', { + runId: 'run-1', + child: { pid: 111 }, + request: { model: 'gpt-5.4' }, + processKilled: false, + cancelRequested: false, + spawnContext: null, + }); + vi.mocked(listTmuxPanePidsForCurrentPlatform).mockResolvedValueOnce(new Map([['%1', 222]])); + + vi.mocked(pidusage) + .mockRejectedValueOnce(new Error('ps: process exited')) + .mockResolvedValueOnce(createPidusageStat(111, 123_000_000) as any) + .mockResolvedValueOnce(createPidusageStat(222, 456_000_000) as any); + + const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team'); + + expect(pidusage).toHaveBeenNthCalledWith(1, [111, 222], { maxage: 0 }); + expect(pidusage).toHaveBeenNthCalledWith(2, 111, { maxage: 0 }); + expect(pidusage).toHaveBeenNthCalledWith(3, 222, { maxage: 0 }); + expect(snapshot.members['team-lead']?.rssBytes).toBe(123_000_000); + expect(snapshot.members.alice?.rssBytes).toBe(456_000_000); + }); + }); + it('removes generated MCP config when createTeam spawn fails synchronously', async () => { allowConsoleLogs(); vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/mock/claude');