diff --git a/.gitignore b/.gitignore index 3fa6fd9c0..f61c5d5f9 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ scripts/session-stats/edit-analysis.csv # parallel-agent worktrees .wt/ +CPU*.md diff --git a/Cargo.lock b/Cargo.lock index e1b3c095a..da9d92c2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2162,7 +2162,7 @@ dependencies = [ [[package]] name = "pi-natives" -version = "14.5.12" +version = "14.5.13" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index 67f9cedec..9fd08513b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["crates/brush-core-vendored", "crates/brush-builtins-vendored"] resolver = "3" [workspace.package] -version = "14.5.12" +version = "14.5.13" edition = "2024" license = "MIT" authors = ["Can Boluk"] diff --git a/bun.lock b/bun.lock index 522b4bf69..fb0743edc 100644 --- a/bun.lock +++ b/bun.lock @@ -16,7 +16,7 @@ }, "packages/agent": { "name": "@oh-my-pi/pi-agent-core", - "version": "14.5.12", + "version": "14.5.13", "dependencies": { "@oh-my-pi/pi-ai": "catalog:", "@oh-my-pi/pi-natives": "catalog:", @@ -29,7 +29,7 @@ }, "packages/ai": { "name": "@oh-my-pi/pi-ai", - "version": "14.5.12", + "version": "14.5.13", "bin": { "pi-ai": "./src/cli.ts", }, @@ -56,7 +56,7 @@ }, "packages/coding-agent": { "name": "@oh-my-pi/pi-coding-agent", - "version": "14.5.12", + "version": "14.5.13", "bin": { "omp": "src/cli.ts", }, @@ -92,7 +92,7 @@ }, "packages/natives": { "name": "@oh-my-pi/pi-natives", - "version": "14.5.12", + "version": "14.5.13", "devDependencies": { "@napi-rs/cli": "catalog:", "@types/bun": "catalog:", @@ -100,7 +100,7 @@ }, "packages/stats": { "name": "@oh-my-pi/omp-stats", - "version": "14.5.12", + "version": "14.5.13", "bin": { "omp-stats": "./src/index.ts", }, @@ -125,7 +125,7 @@ }, "packages/swarm-extension": { "name": "@oh-my-pi/swarm-extension", - "version": "14.5.12", + "version": "14.5.13", "bin": { "omp-swarm": "src/cli.ts", }, @@ -141,7 +141,7 @@ }, "packages/tui": { "name": "@oh-my-pi/pi-tui", - "version": "14.5.12", + "version": "14.5.13", "dependencies": { "@oh-my-pi/pi-natives": "catalog:", "@oh-my-pi/pi-utils": "catalog:", @@ -181,7 +181,7 @@ }, "packages/utils": { "name": "@oh-my-pi/pi-utils", - "version": "14.5.12", + "version": "14.5.13", "dependencies": { "beautiful-mermaid": "catalog:", "handlebars": "catalog:", @@ -212,13 +212,13 @@ "@google/genai": "^1.50.1", "@mozilla/readability": "^0.6.0", "@napi-rs/cli": "3.6.2", - "@oh-my-pi/omp-stats": "14.5.12", - "@oh-my-pi/pi-agent-core": "14.5.12", - "@oh-my-pi/pi-ai": "14.5.12", - "@oh-my-pi/pi-coding-agent": "14.5.12", - "@oh-my-pi/pi-natives": "14.5.12", - "@oh-my-pi/pi-tui": "14.5.12", - "@oh-my-pi/pi-utils": "14.5.12", + "@oh-my-pi/omp-stats": "14.5.13", + "@oh-my-pi/pi-agent-core": "14.5.13", + "@oh-my-pi/pi-ai": "14.5.13", + "@oh-my-pi/pi-coding-agent": "14.5.13", + "@oh-my-pi/pi-natives": "14.5.13", + "@oh-my-pi/pi-tui": "14.5.13", + "@oh-my-pi/pi-utils": "14.5.13", "@puppeteer/browsers": "^2.13.0", "@sinclair/typebox": "^0.34.49", "@smithy/node-http-handler": "^4.6.1", @@ -279,7 +279,7 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1039.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.7", "@aws-sdk/credential-provider-node": "^3.972.38", "@aws-sdk/eventstream-handler-node": "^3.972.14", "@aws-sdk/middleware-eventstream": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.37", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/token-providers": "3.1039.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.23", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.49", "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-rpm9rGcv95ulprNIu/ruhreG4bSKq7oFrErM1Nkp9Cq/zzo/11Hw1/ffYKLM/PAcMGZ+5/zAHOCWBDQ3W1lIBw=="], + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1040.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.7", "@aws-sdk/credential-provider-node": "^3.972.38", "@aws-sdk/eventstream-handler-node": "^3.972.14", "@aws-sdk/middleware-eventstream": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.37", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/token-providers": "3.1040.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.23", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.49", "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tFCqtci1gVGIRwgK3tmv2DV2EawXjBIQgwM/7KaeL4wHUMhNMUA+POUw6vGowtQb51ZaSDjK3KzI3MaQskOyuw=="], "@aws-sdk/core": ["@aws-sdk/core@3.974.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.22", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-YhRC90ofz5oolTJZlA8voU/oUrCB2azi8Usx51k8hhB5LpWbYQMMXKUqSqkoL0Cru+RQJgWTHpAfEDDIwfUhJw=="], @@ -321,7 +321,7 @@ "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.24", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.36", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-amP7tLikppN940wbBFISYqiuzVmpzMS9U3mcgtmVLjX4fdWI/SNCvrXv6ZxfVzTT4cT0rPKOLhFah2xLwzREWw=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1039.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.7", "@aws-sdk/nested-clients": "^3.997.5", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1040.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.7", "@aws-sdk/nested-clients": "^3.997.5", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0KTpz2KqASQwzLOywV1bS2TX6Su0bARkATgpSu236BDM/D/6cMQ2EPiFwoRYwwvXsWSDn8KkKp9NV2ZWWA53Xw=="], "@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], @@ -351,7 +351,7 @@ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], @@ -779,13 +779,13 @@ "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="], + "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="], "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], "bare-fs": ["bare-fs@4.7.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw=="], - "bare-os": ["bare-os@3.9.0", "", {}, "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q=="], + "bare-os": ["bare-os@3.9.1", "", {}, "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ=="], "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], @@ -993,7 +993,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.1.1", "", {}, "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw=="], + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -1095,7 +1095,7 @@ "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], @@ -1141,7 +1141,7 @@ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "postcss": ["postcss@8.5.12", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA=="], + "postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="], "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], @@ -1317,6 +1317,8 @@ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1039.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.7", "@aws-sdk/nested-clients": "^3.997.5", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw=="], + "@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], "@puppeteer/browsers/proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], diff --git a/package.json b/package.json index e9004c61a..8dce24f6a 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,13 @@ "@google/genai": "^1.50.1", "@mozilla/readability": "^0.6.0", "@napi-rs/cli": "3.6.2", - "@oh-my-pi/omp-stats": "14.5.12", - "@oh-my-pi/pi-agent-core": "14.5.12", - "@oh-my-pi/pi-ai": "14.5.12", - "@oh-my-pi/pi-coding-agent": "14.5.12", - "@oh-my-pi/pi-natives": "14.5.12", - "@oh-my-pi/pi-tui": "14.5.12", - "@oh-my-pi/pi-utils": "14.5.12", + "@oh-my-pi/omp-stats": "14.5.13", + "@oh-my-pi/pi-agent-core": "14.5.13", + "@oh-my-pi/pi-ai": "14.5.13", + "@oh-my-pi/pi-coding-agent": "14.5.13", + "@oh-my-pi/pi-natives": "14.5.13", + "@oh-my-pi/pi-tui": "14.5.13", + "@oh-my-pi/pi-utils": "14.5.13", "@puppeteer/browsers": "^2.13.0", "@sinclair/typebox": "^0.34.49", "@smithy/node-http-handler": "^4.6.1", diff --git a/packages/agent/package.json b/packages/agent/package.json index 326776728..9fdd7fa0b 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-agent-core", - "version": "14.5.12", + "version": "14.5.13", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 50d610441..073f6f1dc 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,14 +2,16 @@ ## [Unreleased] -### Breaking Changes - -- Removed `utils/oauth` re-exports from the package entrypoint, so OAuth helper imports from the root module must be updated - ### Fixed - Fixed Vercel AI Gateway dynamic model discovery to use the versioned `/v1/models` catalog endpoint, restoring newly published models. +## [14.5.13] - 2026-05-01 + +### Breaking Changes + +- Removed `utils/oauth` re-exports from the package entrypoint, so OAuth helper imports from the root module must be updated + ## [14.5.10] - 2026-04-30 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 0f404fe15..d887c7e93 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-ai", - "version": "14.5.12", + "version": "14.5.13", "description": "Unified LLM API with automatic model discovery and provider configuration", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/ai/src/models.json b/packages/ai/src/models.json index 6a7f25284..d0dd21af7 100644 --- a/packages/ai/src/models.json +++ b/packages/ai/src/models.json @@ -16058,6 +16058,25 @@ "contextWindow": 222222, "maxTokens": 8888 }, + "x-ai/grok-4.3": { + "id": "x-ai/grok-4.3", + "name": "xAI: Grok 4.3", + "api": "openai-completions", + "provider": "kilo", + "baseUrl": "https://api.kilo.ai/api/gateway", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, "x-ai/grok-code-fast-1": { "id": "x-ai/grok-code-fast-1", "name": "Grok Code Fast 1", @@ -24027,6 +24046,25 @@ "contextWindow": 222222, "maxTokens": 8888 }, + "ibm-granite/granite-4.1-8b": { + "id": "ibm-granite/granite-4.1-8b", + "name": "ibm-granite/granite-4.1-8b", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, "inclusionai/ling-2.6-1t": { "id": "inclusionai/ling-2.6-1t", "name": "inclusionai/ling-2.6-1t", @@ -25827,7 +25865,7 @@ "api": "openai-completions", "provider": "nanogpt", "baseUrl": "https://nano-gpt.com/api/v1", - "reasoning": false, + "reasoning": true, "input": [ "text" ], @@ -25838,7 +25876,12 @@ "cacheWrite": 0 }, "contextWindow": 222222, - "maxTokens": 8888 + "maxTokens": 8888, + "thinking": { + "mode": "effort", + "minLevel": "minimal", + "maxLevel": "xhigh" + } }, "mistral/mistral-vibe-cli-latest": { "id": "mistral/mistral-vibe-cli-latest", @@ -27948,6 +27991,25 @@ "maxLevel": "xhigh" } }, + "owl": { + "id": "owl", + "name": "owl", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, "pamanseau/OpenReasoning-Nemotron-32B": { "id": "pamanseau/OpenReasoning-Nemotron-32B", "name": "pamanseau/OpenReasoning-Nemotron-32B", @@ -28885,6 +28947,25 @@ "maxLevel": "high" } }, + "Qwen3.5-27B-Anko": { + "id": "Qwen3.5-27B-Anko", + "name": "Qwen3.5-27B-Anko", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, "Qwen3.5-27B-BlueStar-Derestricted": { "id": "Qwen3.5-27B-BlueStar-Derestricted", "name": "Qwen3.5-27B-BlueStar-Derestricted", @@ -28961,6 +29042,44 @@ "contextWindow": 222222, "maxTokens": 8888 }, + "Qwen3.5-27B-BlueStar-v3-Derestricted": { + "id": "Qwen3.5-27B-BlueStar-v3-Derestricted", + "name": "Qwen3.5-27B-BlueStar-v3-Derestricted", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-BlueStar-v3-Derestricted-Lite": { + "id": "Qwen3.5-27B-BlueStar-v3-Derestricted-Lite", + "name": "Qwen3.5-27B-BlueStar-v3-Derestricted-Lite", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, "Qwen3.5-27B-Derestricted": { "id": "Qwen3.5-27B-Derestricted", "name": "Qwen3.5-27B-Derestricted", @@ -28980,6 +29099,139 @@ "contextWindow": 222222, "maxTokens": 8888 }, + "Qwen3.5-27B-earica-Derestricted": { + "id": "Qwen3.5-27B-earica-Derestricted", + "name": "Qwen3.5-27B-earica-Derestricted", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-earica-Derestricted-Lite": { + "id": "Qwen3.5-27B-earica-Derestricted-Lite", + "name": "Qwen3.5-27B-earica-Derestricted-Lite", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-Infracelestial": { + "id": "Qwen3.5-27B-Infracelestial", + "name": "Qwen3.5-27B-Infracelestial", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-Marvin-DPO-V2-Derestricted": { + "id": "Qwen3.5-27B-Marvin-DPO-V2-Derestricted", + "name": "Qwen3.5-27B-Marvin-DPO-V2-Derestricted", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-Marvin-DPO-V2-Derestricted-Lite": { + "id": "Qwen3.5-27B-Marvin-DPO-V2-Derestricted-Lite", + "name": "Qwen3.5-27B-Marvin-DPO-V2-Derestricted-Lite", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-Marvin-V2-Derestricted": { + "id": "Qwen3.5-27B-Marvin-V2-Derestricted", + "name": "Qwen3.5-27B-Marvin-V2-Derestricted", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-Marvin-V2-Derestricted-Lite": { + "id": "Qwen3.5-27B-Marvin-V2-Derestricted-Lite", + "name": "Qwen3.5-27B-Marvin-V2-Derestricted-Lite", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, "Qwen3.5-27B-Musica-v1": { "id": "Qwen3.5-27B-Musica-v1", "name": "Qwen3.5-27B-Musica-v1", @@ -28999,6 +29251,44 @@ "contextWindow": 222222, "maxTokens": 8888 }, + "Qwen3.5-27B-NaNovel-Derestricted": { + "id": "Qwen3.5-27B-NaNovel-Derestricted", + "name": "Qwen3.5-27B-NaNovel-Derestricted", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-NaNovel-Derestricted-Lite": { + "id": "Qwen3.5-27B-NaNovel-Derestricted-Lite", + "name": "Qwen3.5-27B-NaNovel-Derestricted-Lite", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, "Qwen3.5-27B-Omega-Evolution-v2.0-Derestricted": { "id": "Qwen3.5-27B-Omega-Evolution-v2.0-Derestricted", "name": "Qwen3.5-27B-Omega-Evolution-v2.0-Derestricted", @@ -29037,6 +29327,63 @@ "contextWindow": 222222, "maxTokens": 8888 }, + "Qwen3.5-27B-Queen-Derestricted": { + "id": "Qwen3.5-27B-Queen-Derestricted", + "name": "Qwen3.5-27B-Queen-Derestricted", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-Queen-Derestricted-Lite": { + "id": "Qwen3.5-27B-Queen-Derestricted-Lite", + "name": "Qwen3.5-27B-Queen-Derestricted-Lite", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, + "Qwen3.5-27B-RpRMax-v1": { + "id": "Qwen3.5-27B-RpRMax-v1", + "name": "Qwen3.5-27B-RpRMax-v1", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, "Qwen3.5-27B-Vivid-Durian": { "id": "Qwen3.5-27B-Vivid-Durian", "name": "Qwen3.5-27B-Vivid-Durian", @@ -31292,6 +31639,25 @@ "contextWindow": 222222, "maxTokens": 8888 }, + "x-ai/grok-4.3": { + "id": "x-ai/grok-4.3", + "name": "x-ai/grok-4.3", + "api": "openai-completions", + "provider": "nanogpt", + "baseUrl": "https://nano-gpt.com/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 222222, + "maxTokens": 8888 + }, "x-ai/grok-code-fast-1": { "id": "x-ai/grok-code-fast-1", "name": "Grok Code Fast 1", @@ -34863,6 +35229,7 @@ }, "contextWindow": 272000, "maxTokens": 128000, + "preferWebsockets": true, "priority": 6, "thinking": { "mode": "effort", @@ -42087,9 +42454,9 @@ "text" ], "cost": { - "input": 0.14, + "input": 0.12, "output": 0.7999999999999999, - "cacheRead": 0.09, + "cacheRead": 0.07, "cacheWrite": 0 }, "contextWindow": 262144, @@ -43265,8 +43632,8 @@ "image" ], "cost": { - "input": 2, - "output": 6, + "input": 1.25, + "output": 2.5, "cacheRead": 0.19999999999999998, "cacheWrite": 0 }, @@ -43303,6 +43670,31 @@ "maxLevel": "high" } }, + "x-ai/grok-4.3": { + "id": "x-ai/grok-4.3", + "name": "xAI: Grok 4.3", + "api": "openai-completions", + "provider": "openrouter", + "baseUrl": "https://openrouter.ai/api/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 1.25, + "output": 2.5, + "cacheRead": 0.19999999999999998, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 8888, + "thinking": { + "mode": "effort", + "minLevel": "minimal", + "maxLevel": "high" + } + }, "x-ai/grok-code-fast-1": { "id": "x-ai/grok-code-fast-1", "name": "Grok Code Fast 1", @@ -44033,6 +44425,25 @@ "contextWindow": 262144, "maxTokens": 8192 }, + "hf:moonshotai/Kimi-K2.6": { + "id": "hf:moonshotai/Kimi-K2.6", + "name": "moonshotai/Kimi-K2.6", + "api": "openai-completions", + "provider": "synthetic", + "baseUrl": "https://api.synthetic.new/openai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 8192 + }, "hf:nvidia/Kimi-K2.5-NVFP4": { "id": "hf:nvidia/Kimi-K2.5-NVFP4", "name": "nvidia/Kimi-K2.5-NVFP4", @@ -45320,6 +45731,28 @@ "supportsUsageInStreaming": false } }, + "grok-4-3": { + "id": "grok-4-3", + "name": "grok-4-3", + "api": "openai-completions", + "provider": "venice", + "baseUrl": "https://api.venice.ai/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 8888, + "compat": { + "supportsUsageInStreaming": false + } + }, "grok-41-fast": { "id": "grok-41-fast", "name": "Grok 4.1 Fast", @@ -50028,6 +50461,31 @@ "maxLevel": "xhigh" } }, + "xai/grok-4.3": { + "id": "xai/grok-4.3", + "name": "Grok 4.3", + "api": "anthropic-messages", + "provider": "vercel-ai-gateway", + "baseUrl": "https://ai-gateway.vercel.sh", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 1.25, + "output": 2.5, + "cacheRead": 0.19999999999999998, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 1000000, + "thinking": { + "mode": "budget", + "minLevel": "minimal", + "maxLevel": "xhigh" + } + }, "xai/grok-code-fast-1": { "id": "xai/grok-code-fast-1", "name": "Grok Code Fast 1", @@ -50986,6 +51444,31 @@ "maxLevel": "high" } }, + "grok-4.3": { + "id": "grok-4.3", + "name": "Grok 4.3", + "api": "openai-completions", + "provider": "xai", + "baseUrl": "https://api.x.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 1.25, + "output": 2.5, + "cacheRead": 0.2, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 30000, + "thinking": { + "mode": "effort", + "minLevel": "minimal", + "maxLevel": "high" + } + }, "grok-beta": { "id": "grok-beta", "name": "Grok Beta", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index abacab294..afd082d6c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] + +## [14.5.13] - 2026-05-01 + ### Breaking Changes - Removed the built-in `python` tool in favor of `eval`, so tool allowlists and tool-call handlers referencing `python` need to migrate @@ -19,11 +22,18 @@ ### Changed +- Changed AGENTS.md discovery to respect `.gitignore` files during project context collection so ignored context files are no longer loaded +- Changed eval tool initialization to skip Python kernel preflight when the JavaScript backend is enabled, avoiding unnecessary startup checks +- Changed model registry refresh flow to defer rebuilding the canonical model index until refresh operations complete, reducing refresh churn - Changed execution/tool discovery flow so `exec` maps to `eval` when any `eval` backend is enabled, while `bash` stays independently available - Changed `eval` dispatch to automatically fall back to JavaScript when Python is unavailable and JavaScript backend is enabled - Parallelized plugin root preloading with other startup initialization in `runRootCommand` to reduce startup latency - Parallelized session bootstrap work in `createAgentSession`, including AGENTS.md scanning, context discovery, prompt template loading, slash command loading, and skill discovery, to reduce time to first available session +### Fixed + +- Fixed eval startup messaging to report `eval` as unavailable when Python is unreachable and JavaScript backend is disabled + ## [14.5.12] - 2026-04-30 ### Breaking Changes diff --git a/packages/coding-agent/DEVELOPMENT.md b/packages/coding-agent/DEVELOPMENT.md index 9c54725ca..10e280e79 100644 --- a/packages/coding-agent/DEVELOPMENT.md +++ b/packages/coding-agent/DEVELOPMENT.md @@ -391,7 +391,7 @@ A `ToolFactory` is `(session: ToolSession) => Tool | null | Promise 1. Normalizes requested tool names (`toolNames`) and always injects `exit_plan_mode`. 2. Resolves eval backend allowance via `PI_PY` override (`getEvalBackendsFromEnv()`) or `eval.py` / `eval.js` settings. -3. Performs Python kernel preflight/warmup when applicable (`checkPythonKernelAvailability`, `warmPythonEnvironment`). +3. Performs Python kernel preflight when applicable (`checkPythonKernelAvailability`). 4. Computes effective gating (`isToolAllowed`) from settings and runtime state: - feature toggles (`find.enabled`, `grep.enabled`, etc.) - recursion guard for `task` (`task.maxRecursionDepth` vs `session.taskDepth`) @@ -777,7 +777,7 @@ This subsystem is split into two layers: - Entry points: - `executePython(code, options)` - `executePythonWithKernel(kernel, code, options)` - - warmup/session utilities (`warmPythonEnvironment`, `disposeAllKernelSessions`). + - session utilities (`disposeAllKernelSessions`, `disposeKernelSessionsByOwner`). - Manages kernel session lifecycle in `kernelSessions: Map` with: - bounded session count (`MAX_KERNEL_SESSIONS`), LRU eviction (`evictOldestSession`) - idle cleanup timer (`cleanupIdleSessions`) diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index d5d70c556..9609864dd 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-coding-agent", - "version": "14.5.12", + "version": "14.5.13", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 1e4156bf7..9872fa7b6 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -175,6 +175,15 @@ export class ConfigFile implements IConfigFile { return result; } + getMtimeMs(): number | null { + try { + return fs.statSync(this.path()).mtimeMs; + } catch (err) { + if (isEnoent(err)) return null; + throw err; + } + } + withValidation(name: string, validate: (value: T) => void): this { const prev = this.#auxValidate; this.#auxValidate = (value: T) => { diff --git a/packages/coding-agent/src/config/model-equivalence.ts b/packages/coding-agent/src/config/model-equivalence.ts index 47fb6984c..ac781a82b 100644 --- a/packages/coding-agent/src/config/model-equivalence.ts +++ b/packages/coding-agent/src/config/model-equivalence.ts @@ -61,7 +61,25 @@ const TRAILING_CANONICAL_MARKERS = [ "int8", "int4", ] as const; +const TRAILING_MARKER_SUFFIXES: readonly string[] = (() => { + const suffixes: string[] = []; + for (const marker of TRAILING_CANONICAL_MARKERS) { + const lower = marker.toLowerCase(); + suffixes.push(`-${lower}`, `:${lower}`); + } + return suffixes; +})(); const WRAPPER_PREFIXES = ["duo-chat-"] as const; + +let __referenceDataCache: CanonicalReferenceData | undefined; +const EMPTY_COMPILED_EQUIVALENCE: CompiledEquivalenceConfig = { + overrides: new Map(), + exclude: new Set(), +}; +const __resolutionCache: WeakMap< + CompiledEquivalenceConfig, + WeakMap, ResolvedCanonicalModel> +> = new WeakMap(); const FAMILY_EXTRACTION_PATTERNS = [ /(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+)(?::|$)/i, /(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+(?:[-_/][a-z0-9.]+)*)(?::|$)/i, @@ -79,6 +97,9 @@ function shouldReplaceReference(existing: Model | undefined, candidate: Mod } function createCanonicalReferenceData(): CanonicalReferenceData { + if (__referenceDataCache) { + return __referenceDataCache; + } const references = new Map>(); for (const provider of getBundledProviders()) { for (const model of getBundledModels(provider as Parameters[0])) { @@ -89,10 +110,12 @@ function createCanonicalReferenceData(): CanonicalReferenceData { } } } - return { - references, - officialIds: new Set(references.keys()), + const officialIds = new Set(references.keys()); + __referenceDataCache = { + references: Object.freeze(references) as Map>, + officialIds: Object.freeze(officialIds) as Set, }; + return __referenceDataCache; } function normalizeSelectorKey(selector: string): string { @@ -135,10 +158,12 @@ function buildExclusionSet(exclusions: readonly string[] | undefined): Set, candidate: string): void { @@ -149,12 +174,10 @@ function addCanonicalCandidate(candidates: Set, candidate: string): void } function stripTrailingMarker(candidate: string): string | undefined { - for (const marker of TRAILING_CANONICAL_MARKERS) { - for (const separator of ["-", ":"] as const) { - const suffix = `${separator}${marker}`; - if (candidate.toLowerCase().endsWith(suffix)) { - return candidate.slice(0, -suffix.length); - } + const lower = candidate.toLowerCase(); + for (const suffix of TRAILING_MARKER_SUFFIXES) { + if (lower.endsWith(suffix)) { + return candidate.slice(0, -suffix.length); } } return undefined; @@ -450,8 +473,8 @@ function getHeuristicCanonicalCandidates(modelId: string): string[] { const queue = [modelId]; const visited = new Set(); - while (queue.length > 0) { - const candidate = queue.shift(); + for (let qi = 0; qi < queue.length; qi += 1) { + const candidate = queue[qi]; if (!candidate) { continue; } @@ -644,8 +667,18 @@ export function buildCanonicalModelIndex( const byId = new Map(); const bySelector = new Map(); + let modelCache = __resolutionCache.get(compiledEquivalence); + if (!modelCache) { + modelCache = new WeakMap, ResolvedCanonicalModel>(); + __resolutionCache.set(compiledEquivalence, modelCache); + } + for (const model of models) { - const canonical = resolveCanonicalIdForModel(model, compiledEquivalence, referenceData); + let canonical = modelCache.get(model); + if (!canonical) { + canonical = resolveCanonicalIdForModel(model, compiledEquivalence, referenceData); + modelCache.set(model, canonical); + } const selector = formatCanonicalVariantSelector(model); const variant: CanonicalModelVariant = { canonicalId: canonical.id, diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index 3da123c44..54f807cc5 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -797,6 +797,7 @@ export class ModelRegistry { #equivalenceConfig: ModelEquivalenceConfig | undefined; #configError: ConfigError | undefined = undefined; #modelsConfigFile: ConfigFile; + #lastStaticLoadMtime: number | null = null; #registeredProviderSources: Set = new Set(); #providerDiscoveryStates: Map = new Map(); #cacheDbPath?: string; @@ -810,6 +811,8 @@ export class ModelRegistry { #runtimeProviderOverrides: Map = new Map(); #runtimeProvidersBySource: Map> = new Map(); #runtimeProviderSourceByName: Map = new Map(); + #rebuildPending: boolean = false; + #rebuildSuspended: number = 0; /** * @param authStorage - Auth storage for API key resolution @@ -836,9 +839,14 @@ export class ModelRegistry { * Reload models from disk (built-in + custom from models.json). */ async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise { - this.#reloadStaticModels(); - this.#suppressedSelectors.clear(); - await this.#refreshRuntimeDiscoveries(strategy); + this.#suspendRebuild(); + try { + this.#reloadStaticModels(); + this.#suppressedSelectors.clear(); + await this.#refreshRuntimeDiscoveries(strategy); + } finally { + this.#resumeRebuild(); + } } refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached"): void { @@ -860,16 +868,26 @@ export class ModelRegistry { } async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise { - this.#reloadStaticModels(); - for (const selector of this.#suppressedSelectors.keys()) { - if (selector.startsWith(`${providerId}/`)) { - this.#suppressedSelectors.delete(selector); + this.#suspendRebuild(); + try { + this.#reloadStaticModels(); + for (const selector of this.#suppressedSelectors.keys()) { + if (selector.startsWith(`${providerId}/`)) { + this.#suppressedSelectors.delete(selector); + } } + await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId])); + } finally { + this.#resumeRebuild(); } - await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId])); } #reloadStaticModels(): void { + const currentMtime = this.#modelsConfigFile.getMtimeMs(); + if (currentMtime !== null && currentMtime === this.#lastStaticLoadMtime) { + // models.json unchanged since last load; reload + canonical rebuild would be redundant. + return; + } this.#modelsConfigFile.invalidate(); this.#customProviderApiKeys.clear(); this.#keylessProviders.clear(); @@ -924,6 +942,7 @@ export class ModelRegistry { const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides); this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides); this.#rebuildCanonicalIndex(); + this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs(); } /** Load built-in models, applying provider-level overrides only. @@ -947,14 +966,19 @@ export class ModelRegistry { #mergeResolvedModels(baseModels: Model[], replacementModels: Model[]): Model[] { const merged = [...baseModels]; + const indexByKey = new Map(); + for (let i = 0; i < merged.length; i += 1) { + const m = merged[i]; + indexByKey.set(`${m.provider}\u0000${m.id}`, i); + } for (const replacementModel of replacementModels) { - const existingIndex = merged.findIndex( - m => m.provider === replacementModel.provider && m.id === replacementModel.id, - ); - if (existingIndex >= 0) { + const key = `${replacementModel.provider}\u0000${replacementModel.id}`; + const existingIndex = indexByKey.get(key); + if (existingIndex !== undefined) { merged[existingIndex] = replacementModel; } else { merged.push(replacementModel); + indexByKey.set(key, merged.length - 1); } } return merged; @@ -963,9 +987,15 @@ export class ModelRegistry { /** Merge custom models with built-in, replacing by provider+id match */ #mergeCustomModels(builtInModels: Model[], customModels: CustomModelOverlay[]): Model[] { const merged = [...builtInModels]; + const indexByKey = new Map(); + for (let i = 0; i < merged.length; i += 1) { + const m = merged[i]; + indexByKey.set(`${m.provider}\u0000${m.id}`, i); + } for (const customModel of customModels) { - const existingIndex = merged.findIndex(m => m.provider === customModel.provider && m.id === customModel.id); - if (existingIndex >= 0) { + const key = `${customModel.provider}\u0000${customModel.id}`; + const existingIndex = indexByKey.get(key); + if (existingIndex !== undefined) { const existingModel = merged[existingIndex]; merged[existingIndex] = enrichModelThinking({ ...existingModel, @@ -990,6 +1020,7 @@ export class ModelRegistry { } as Model); } else { merged.push(finalizeCustomModel(customModel, { useDefaults: true })); + indexByKey.set(key, merged.length - 1); } } return merged; @@ -1756,7 +1787,26 @@ export class ModelRegistry { } #rebuildCanonicalIndex(): void { + if (this.#rebuildSuspended > 0) { + this.#rebuildPending = true; + return; + } this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig); + this.#rebuildPending = false; + } + + #suspendRebuild(): void { + this.#rebuildSuspended += 1; + } + + #resumeRebuild(): void { + if (this.#rebuildSuspended > 0) { + this.#rebuildSuspended -= 1; + } + if (this.#rebuildSuspended === 0 && this.#rebuildPending) { + this.#rebuildPending = false; + this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig); + } } #parseModels(config: ModelsConfig): CustomModelOverlay[] { @@ -2016,6 +2066,7 @@ export class ModelRegistry { this.#runtimeProviderSourceByName.delete(providerName); this.#clearRuntimeProviderState(providerName); } + this.#lastStaticLoadMtime = null; this.#reloadStaticModels(); this.#rebuildCanonicalIndex(); } @@ -2094,6 +2145,7 @@ export class ModelRegistry { this.#runtimeProviderSourceByName.set(providerName, sourceId); } if (sourceHandoff) { + this.#lastStaticLoadMtime = null; this.#reloadStaticModels(); } diff --git a/packages/coding-agent/src/config/model-resolver.ts b/packages/coding-agent/src/config/model-resolver.ts index 114b874dc..ba29c033f 100644 --- a/packages/coding-agent/src/config/model-resolver.ts +++ b/packages/coding-agent/src/config/model-resolver.ts @@ -116,6 +116,24 @@ function cloneModelWithRequestedId(model: Model, requestedId: string): Mode }; } +const providerModelIndexCache = new WeakMap[], Map | null>>(); + +function getProviderModelIndex(availableModels: readonly Model[]): Map | null> { + let index = providerModelIndexCache.get(availableModels); + if (index) return index; + index = new Map | null>(); + for (const m of availableModels) { + const key = `${m.provider.toLowerCase()}\u0000${m.id.toLowerCase()}`; + if (index.has(key)) { + index.set(key, null); // ambiguous sentinel; do not overwrite back + } else { + index.set(key, m); + } + } + providerModelIndexCache.set(availableModels, index); + return index; +} + export function resolveProviderModelReference( provider: string, modelId: string, @@ -127,14 +145,13 @@ export function resolveProviderModelReference( return undefined; } - const exactMatches = availableModels.filter( - model => model.provider.toLowerCase() === normalizedProvider && model.id.toLowerCase() === normalizedModelId, - ); - if (exactMatches.length === 1) { - return exactMatches[0]; + const index = getProviderModelIndex(availableModels); + const exact = index.get(`${normalizedProvider}\u0000${normalizedModelId}`); + if (exact === null) { + return undefined; // ambiguous } - if (exactMatches.length > 1) { - return undefined; + if (exact !== undefined) { + return exact; } if (normalizedProvider !== "openrouter") { @@ -142,16 +159,13 @@ export function resolveProviderModelReference( } for (const fallbackId of getOpenRouterFallbackModelIds(modelId).slice(1)) { - const baseMatches = availableModels.filter( - model => - model.provider.toLowerCase() === normalizedProvider && model.id.toLowerCase() === fallbackId.toLowerCase(), - ); - if (baseMatches.length === 1) { - return cloneModelWithRequestedId(baseMatches[0], modelId); - } - if (baseMatches.length > 1) { + const fallback = index.get(`${normalizedProvider}\u0000${fallbackId.toLowerCase()}`); + if (fallback === null) { return undefined; } + if (fallback !== undefined) { + return cloneModelWithRequestedId(fallback, modelId); + } } return undefined; diff --git a/packages/coding-agent/src/config/settings.ts b/packages/coding-agent/src/config/settings.ts index b6ed1c432..40add3f47 100644 --- a/packages/coding-agent/src/config/settings.ts +++ b/packages/coding-agent/src/config/settings.ts @@ -398,21 +398,22 @@ export class Settings { // ───────────────────────────────────────────────────────────────────────── async #load(): Promise { + // Project settings load (loadCapability scans cwd) is independent of the + // persist chain (storage open → legacy migration → global config.yml read), + // so kick it off first and await after the persist chain completes. The + // persist steps remain sequential: migration may write config.yml, which + // #loadYaml then reads; migration's db fallback needs #storage opened. + const projectPromise = this.#loadProjectSettings(); + if (this.#persist) { - // Open storage this.#storage = await AgentStorage.open(getAgentDbPath(this.#agentDir)); - - // Migrate from legacy formats if needed await this.#migrateFromLegacy(); - - // Load global settings from config.yml this.#global = await this.#loadYaml(this.#configPath!); } - // Load project settings - this.#project = await this.#loadProjectSettings(); + this.#project = await projectPromise; - // Build merged view + // Build merged view (global → project → overrides; project wins over global) this.#rebuildMerged(); this.#fireAllHooks(); return this; diff --git a/packages/coding-agent/src/eval/backend.ts b/packages/coding-agent/src/eval/backend.ts index badacbd31..80a256829 100644 --- a/packages/coding-agent/src/eval/backend.ts +++ b/packages/coding-agent/src/eval/backend.ts @@ -1,18 +1,14 @@ import type { ToolSession } from "../tools"; import type { EvalDisplayOutput, EvalLanguage } from "./types"; -/** Per-call options shared across warm and execute. */ -export interface ExecutorBackendCallOptions { +/** Per-cell execute() options. */ +export interface ExecutorBackendExecOptions { cwd: string; sessionId: string; sessionFile: string | undefined; kernelOwnerId: string | undefined; signal?: AbortSignal; session: ToolSession; -} - -/** Per-cell execute() options. */ -export interface ExecutorBackendExecOptions extends ExecutorBackendCallOptions { deadlineMs: number; reset: boolean; artifactPath: string | undefined; @@ -40,10 +36,8 @@ export interface ExecutorBackend { readonly label: string; /** Source language identifier passed to the syntax highlighter (e.g. "python", "javascript"). */ readonly highlightLang: string; - /** Cheap availability check (no full warmup). Used by fallback resolution. */ + /** Cheap availability check. Used by fallback resolution. */ isAvailable(session: ToolSession): Promise; - /** Optional pre-warm performed once per session (no-op on backends that don't need it). */ - warm?(opts: ExecutorBackendCallOptions): Promise<{ ok: boolean; reason?: string }>; /** Execute one cell. Caller invokes once per cell and aggregates results. */ execute(code: string, opts: ExecutorBackendExecOptions): Promise; } diff --git a/packages/coding-agent/src/eval/py/executor.ts b/packages/coding-agent/src/eval/py/executor.ts index fda829b50..7aedb1de6 100644 --- a/packages/coding-agent/src/eval/py/executor.ts +++ b/packages/coding-agent/src/eval/py/executor.ts @@ -453,25 +453,6 @@ async function ensureKernelAvailable( } } -export async function warmPythonEnvironment( - cwd: string, - sessionId?: string, - _useSharedGateway?: boolean, - _sessionFile?: string, - kernelOwnerId?: string, - signal?: AbortSignal, -): Promise<{ ok: boolean; reason?: string }> { - const resolvedSessionId = sessionId ?? `session:${cwd}`; - try { - await logger.time("warmPython:ensureKernelAvailable", ensureKernelAvailable, cwd, { signal }); - } catch (err: unknown) { - const reason = err instanceof Error ? err.message : String(err); - return { ok: false, reason }; - } - attachKernelOwner(resolvedSessionId, kernelOwnerId); - return { ok: true }; -} - function isResourceExhaustionError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); return ( diff --git a/packages/coding-agent/src/eval/py/index.ts b/packages/coding-agent/src/eval/py/index.ts index 9e2caff46..8eec1a9a1 100644 --- a/packages/coding-agent/src/eval/py/index.ts +++ b/packages/coding-agent/src/eval/py/index.ts @@ -1,11 +1,6 @@ import type { ToolSession } from "../../tools"; -import type { - ExecutorBackend, - ExecutorBackendCallOptions, - ExecutorBackendExecOptions, - ExecutorBackendResult, -} from "../backend"; -import { executePython, type PythonExecutorOptions, warmPythonEnvironment } from "./executor"; +import type { ExecutorBackend, ExecutorBackendExecOptions, ExecutorBackendResult } from "../backend"; +import { executePython, type PythonExecutorOptions } from "./executor"; import { checkPythonKernelAvailability } from "./kernel"; const PYTHON_SESSION_PREFIX = "python:"; @@ -29,19 +24,6 @@ export default { return availability.ok; }, - async warm(opts: ExecutorBackendCallOptions): Promise<{ ok: boolean; reason?: string }> { - const useSharedGateway = readSetting(opts.session, "python.sharedGateway"); - const result = await warmPythonEnvironment( - opts.cwd, - namespaceSessionId(opts.sessionId), - useSharedGateway, - opts.sessionFile, - opts.kernelOwnerId, - opts.signal, - ); - return { ok: result.ok, reason: result.reason }; - }, - async execute(code: string, opts: ExecutorBackendExecOptions): Promise { const useSharedGateway = readSetting(opts.session, "python.sharedGateway"); const kernelMode = readSetting(opts.session, "python.kernelMode"); diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index d2da63254..abc126f5f 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -242,6 +242,11 @@ async function getChangelogForDisplay(parsed: Args): Promise } const lastVersion = settings.get("lastChangelogVersion"); + if (lastVersion === VERSION) { + // Steady state: user already saw the current version's changelog. Skip the file read + parse. + return undefined; + } + const changelogPath = getChangelogPath(); const entries = await parseChangelog(changelogPath); @@ -627,7 +632,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise {}); const cwd = getProjectDir(); - await logger.time("settings:init", Settings.init, { cwd }); + const settingsInstance = await logger.time("settings:init", Settings.init, { cwd }); if (parsedArgs.mode === "rpc") { applyRpcDefaultSettingOverrides(); } @@ -659,7 +664,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise, -): Promise { - if (depth > AGENTS_MD_MAX_DEPTH || discovered.size >= limit) { - return; - } - - let entries: fs.Dirent[]; - try { - entries = await fs.promises.readdir(dir, { withFileTypes: true }); - } catch { - return; - } - - if (depth >= AGENTS_MD_MIN_DEPTH) { - const hasAgentsMd = entries.some(entry => entry.isFile() && entry.name === "AGENTS.md"); - if (hasAgentsMd) { - const relPath = normalizePath(path.relative(root, path.join(dir, "AGENTS.md"))); - if (relPath.length > 0) { - discovered.add(relPath); - } - if (discovered.size >= limit) { - return; - } - } - } - - if (depth === AGENTS_MD_MAX_DEPTH) { - return; - } - - const childDirs = entries - .filter(entry => entry.isDirectory() && !shouldSkipAgentsDir(entry.name)) - .map(entry => entry.name) - .sort(); - - await Promise.all( - childDirs.map(async child => { - if (discovered.size >= limit) return; - await collectAgentsMdFiles(root, path.join(dir, child), depth + 1, limit, discovered); - }), - ); -} - async function listAgentsMdFiles(root: string, limit: number): Promise { try { - const discovered = new Set(); - await collectAgentsMdFiles(root, root, 0, limit, discovered); - return Array.from(discovered).sort().slice(0, limit); + const result = await glob({ + pattern: "**/AGENTS.md", + path: root, + fileType: FileType.File, + recursive: true, + hidden: false, + gitignore: true, + maxResults: limit * 4, + cache: true, + }); + const files: string[] = []; + for (const m of result.matches) { + const rel = m.path.replace(/\\/g, "/"); + if (!rel?.endsWith("AGENTS.md")) continue; + const segments = rel.split("/"); + const depth = segments.length - 1; + if (depth < AGENTS_MD_MIN_DEPTH || depth > AGENTS_MD_MAX_DEPTH) continue; + const dirSegments = segments.slice(0, -1); + if (dirSegments.some(seg => AGENTS_MD_EXCLUDED_DIRS.has(seg) || seg.startsWith("."))) continue; + files.push(rel); + if (files.length >= limit) break; + } + return Array.from(new Set(files)).sort().slice(0, limit); } catch { return []; } diff --git a/packages/coding-agent/src/tools/eval.ts b/packages/coding-agent/src/tools/eval.ts index 0c842e07f..e6915f9ed 100644 --- a/packages/coding-agent/src/tools/eval.ts +++ b/packages/coding-agent/src/tools/eval.ts @@ -348,8 +348,6 @@ export class EvalTool implements AgentTool { }); const sessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`; - const warmedBackends = new Set(); - for (let i = 0; i < cells.length; i++) { const cell = cells[i]; const backend = cell.resolved.backend; @@ -360,23 +358,6 @@ export class EvalTool implements AgentTool { ? AbortSignal.any([signal, timeoutSignal, sessionAbortController.signal]) : AbortSignal.any([timeoutSignal, sessionAbortController.signal]); - if (!warmedBackends.has(backend.id) && backend.warm) { - const warmup = await backend.warm({ - cwd: session.cwd, - sessionId, - sessionFile: sessionFile ?? undefined, - kernelOwnerId, - signal: combinedSignal, - session, - }); - if (!warmup.ok) { - if (combinedSignal.aborted) throw new ToolAbortError(); - throw new ToolError(warmup.reason ?? `${backend.label} prelude helpers unavailable`); - } - session.assertEvalExecutionAllowed?.(); - } - warmedBackends.add(backend.id); - const cellResult = cellResults[i]; cellResult.status = "running"; cellResult.output = ""; diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts index 1a0b3a441..b678a1163 100644 --- a/packages/coding-agent/src/tools/index.ts +++ b/packages/coding-agent/src/tools/index.ts @@ -1,11 +1,10 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core"; import type { ToolChoice } from "@oh-my-pi/pi-ai"; -import { $env, $flag, isBunTestRuntime, logger } from "@oh-my-pi/pi-utils"; +import { $env, $flag, logger } from "@oh-my-pi/pi-utils"; import type { AsyncJobManager } from "../async"; import type { PromptTemplate } from "../config/prompt-templates"; import type { Settings } from "../config/settings"; import { EditTool } from "../edit"; -import { warmPythonEnvironment } from "../eval/py/executor"; import { checkPythonKernelAvailability } from "../eval/py/kernel"; import type { Skill } from "../extensibility/skills"; import type { InternalUrlRouter } from "../internal-urls"; @@ -108,8 +107,6 @@ export interface ToolSession { hasUI: boolean; /** Skip Python kernel availability check and warmup */ skipPythonPreflight?: boolean; - /** Force Python prelude warmup even when test env would normally skip it */ - forcePythonWarmup?: boolean; /** Pre-loaded context files (AGENTS.md, etc) */ contextFiles?: ContextFileEntry[]; /** Pre-loaded skills */ @@ -297,54 +294,23 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P const allowPython = backends.python; const allowJs = backends.js; const skipPythonPreflight = session.skipPythonPreflight === true; + // Eval tool is enabled if EITHER backend is reachable. We only need to know + // whether python is reachable when JS is disabled — otherwise allowEval is + // already true and the python-availability check can be deferred to first + // invocation of the python backend (already handled inside the executor). let pythonAvailable = true; - const shouldCheckPython = - !skipPythonPreflight && allowPython && (requestedTools === undefined || requestedTools.includes("eval")); - const isTestEnv = isBunTestRuntime(); - const forcePythonWarmup = session.forcePythonWarmup === true; - const skipPythonWarm = (isTestEnv && !forcePythonWarmup) || $flag("PI_PYTHON_SKIP_CHECK"); - if (shouldCheckPython) { + if ( + !skipPythonPreflight && + allowPython && + !allowJs && + (requestedTools === undefined || requestedTools.includes("eval")) + ) { const availability = await logger.time("createTools:pythonCheck", checkPythonKernelAvailability, session.cwd); pythonAvailable = availability.ok; if (!availability.ok) { - logger.warn("Python kernel unavailable; eval will dispatch to JavaScript backend", { + logger.warn("Python kernel unavailable and JS backend disabled; eval will be unavailable", { reason: availability.reason, }); - } else if (!skipPythonWarm) { - const sessionFile = session.getSessionFile?.() ?? undefined; - const kernelOwnerId = session.getEvalKernelOwnerId?.() ?? undefined; - const warmSessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`; - const warmupAbortController = new AbortController(); - try { - session.assertEvalExecutionAllowed?.(); - - const warmupExecution = session.trackEvalExecution - ? logger.time( - "createTools:warmPython", - warmPythonEnvironment, - session.cwd, - warmSessionId, - session.settings.get("python.sharedGateway"), - sessionFile, - kernelOwnerId, - warmupAbortController.signal, - ) - : logger.time( - "createTools:warmPython", - warmPythonEnvironment, - session.cwd, - warmSessionId, - session.settings.get("python.sharedGateway"), - sessionFile, - kernelOwnerId, - ); - await (session.trackEvalExecution?.(warmupExecution, warmupAbortController) ?? warmupExecution); - session.assertEvalExecutionAllowed?.(); - } catch (err) { - logger.warn("Failed to warm Python environment", { - error: err instanceof Error ? err.message : String(err), - }); - } } } diff --git a/packages/coding-agent/src/tools/recipe/runners/pkg.ts b/packages/coding-agent/src/tools/recipe/runners/pkg.ts index 57dd86f3d..eedca6707 100644 --- a/packages/coding-agent/src/tools/recipe/runners/pkg.ts +++ b/packages/coding-agent/src/tools/recipe/runners/pkg.ts @@ -10,21 +10,19 @@ interface PackageJsonInfo { } async function resolvePackageRunner(cwd: string): Promise { - if ((await isFile(path.join(cwd, "bun.lock"))) || (await isFile(path.join(cwd, "bun.lockb")))) { - return "bun run"; - } - if (await isFile(path.join(cwd, "pnpm-lock.yaml"))) { - return "pnpm run"; - } - if (await isFile(path.join(cwd, "yarn.lock"))) { - return "yarn"; - } - if ((await isFile(path.join(cwd, "package-lock.json"))) || (await isFile(path.join(cwd, "npm-shrinkwrap.json")))) { - return "npm run"; - } - if ($which("bun")) { - return "bun run"; - } + const [bunLock, bunLockb, pnpmLock, yarnLock, npmLock, npmShrink] = await Promise.all([ + isFile(path.join(cwd, "bun.lock")), + isFile(path.join(cwd, "bun.lockb")), + isFile(path.join(cwd, "pnpm-lock.yaml")), + isFile(path.join(cwd, "yarn.lock")), + isFile(path.join(cwd, "package-lock.json")), + isFile(path.join(cwd, "npm-shrinkwrap.json")), + ]); + if (bunLock || bunLockb) return "bun run"; + if (pnpmLock) return "pnpm run"; + if (yarnLock) return "yarn"; + if (npmLock || npmShrink) return "npm run"; + if ($which("bun")) return "bun run"; return "npm run"; } @@ -86,18 +84,23 @@ async function readPackageJson(filePath: string): Promise { const includePatterns = patterns.filter(pattern => !pattern.startsWith("!")).map(normalizeWorkspacePattern); const excludePatterns = patterns.filter(pattern => pattern.startsWith("!")).map(normalizeWorkspacePattern); - const excluded = new Set(); - for (const pattern of excludePatterns) { - for await (const entry of new Bun.Glob(pattern.slice(1)).scan({ cwd, onlyFiles: true })) { - excluded.add(path.normalize(String(entry))); - } - } - const files = new Set(); - for (const pattern of includePatterns) { + + const collect = async (pattern: string): Promise => { + const out: string[] = []; for await (const entry of new Bun.Glob(pattern).scan({ cwd, onlyFiles: true })) { - const normalized = path.normalize(String(entry)); - if (normalized !== "package.json" && !excluded.has(normalized)) files.add(normalized); + out.push(path.normalize(String(entry))); } + return out; + }; + + const [excludedLists, includedLists] = await Promise.all([ + Promise.all(excludePatterns.map(pattern => collect(pattern.slice(1)))), + Promise.all(includePatterns.map(pattern => collect(pattern))), + ]); + const excluded = new Set(excludedLists.flat()); + const files = new Set(); + for (const entry of includedLists.flat()) { + if (entry !== "package.json" && !excluded.has(entry)) files.add(entry); } return [...files].sort((left, right) => left.localeCompare(right)); } @@ -132,10 +135,10 @@ async function readPackageTasks(cwd: string): Promise { ); } - for (const packageJsonPath of workspacePackageJsons) { - const pkg = await readPackageJson(path.join(cwd, packageJsonPath)); - if (!pkg || pkg.scripts.length === 0) continue; - const packageDir = path.dirname(packageJsonPath); + const pkgs = await Promise.all(workspacePackageJsons.map(p => readPackageJson(path.join(cwd, p)))); + pkgs.forEach((pkg, index) => { + if (!pkg || pkg.scripts.length === 0) return; + const packageDir = path.dirname(workspacePackageJsons[index]); tasks.push( ...tasksForPackage({ pkg, @@ -143,7 +146,7 @@ async function readPackageTasks(cwd: string): Promise { namespaced: true, }), ); - } + }); return tasks.length > 0 ? tasks : null; } @@ -153,8 +156,7 @@ export const pkgRunner: TaskRunner = { label: "Pkg", async detect(cwd: string): Promise { try { - const commandPrefix = await resolvePackageRunner(cwd); - const tasks = await readPackageTasks(cwd); + const [commandPrefix, tasks] = await Promise.all([resolvePackageRunner(cwd), readPackageTasks(cwd)]); if (!tasks || tasks.length === 0) return null; return { id: "pkg", label: "Pkg", commandPrefix, tasks }; } catch (err) { diff --git a/packages/coding-agent/src/utils/markit.ts b/packages/coding-agent/src/utils/markit.ts index 107558c57..ab622c957 100644 --- a/packages/coding-agent/src/utils/markit.ts +++ b/packages/coding-agent/src/utils/markit.ts @@ -1,6 +1,5 @@ import { untilAborted } from "@oh-my-pi/pi-utils"; -import type { StreamInfo } from "markit-ai"; -import { Markit } from "markit-ai"; +import type { Markit, StreamInfo } from "markit-ai"; import { ToolAbortError } from "../tools/tool-errors"; export interface MarkitConversionResult { @@ -9,7 +8,15 @@ export interface MarkitConversionResult { error?: string; } -const markit = new Markit(); +let markit: () => Markit | Promise = async () => { + const promise = import("markit-ai").then(({ Markit }) => { + const instance = new Markit(); + markit = () => instance; + return instance; + }); + markit = () => promise; + return promise; +}; function normalizeExtension(extension: string): string { const trimmed = extension.trim().toLowerCase(); @@ -24,9 +31,10 @@ function normalizeError(error: unknown): string { return "Conversion failed"; } -async function runMarkitConversion(task: () => Promise, signal?: AbortSignal): Promise { +async function runMarkitConversion(task: (markit: Markit) => Promise, signal?: AbortSignal): Promise { try { - return signal ? await untilAborted(signal, task) : await task(); + const instance = await markit(); + return signal ? await untilAborted(signal, () => task(instance)) : await task(instance); } catch (error) { if (error instanceof ToolAbortError) { throw error; @@ -48,7 +56,7 @@ function finalizeConversion(markdown?: string): MarkitConversionResult { export async function convertFileWithMarkit(filePath: string, signal?: AbortSignal): Promise { try { - const result = await runMarkitConversion(() => markit.convertFile(filePath), signal); + const result = await runMarkitConversion(markit => markit.convertFile(filePath), signal); return finalizeConversion(result.markdown); } catch (error) { if (error instanceof ToolAbortError) { @@ -70,7 +78,7 @@ export async function convertBufferWithMarkit( }; try { - const result = await runMarkitConversion(() => markit.convert(Buffer.from(buffer), streamInfo), signal); + const result = await runMarkitConversion(markit => markit.convert(Buffer.from(buffer), streamInfo), signal); return finalizeConversion(result.markdown); } catch (error) { if (error instanceof ToolAbortError) { diff --git a/packages/coding-agent/test/agent-session-python-cleanup.test.ts b/packages/coding-agent/test/agent-session-python-cleanup.test.ts index 2818acbe3..d0eae4e7c 100644 --- a/packages/coding-agent/test/agent-session-python-cleanup.test.ts +++ b/packages/coding-agent/test/agent-session-python-cleanup.test.ts @@ -95,18 +95,15 @@ const createSession = async ( slashCommands: [], enableMCP: false, enableLsp: false, - forcePythonWarmup: true, toolNames: ["eval"], }) ).session; -const stubPythonWarmup = () => vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true }); - -const createWarmupKernel = () => { +const createMockKernel = () => { let alive = true; return { execute: vi.fn(async () => { - if (!alive) throw new Error("Expected warmup kernel to be restarted after shutdown"); + if (!alive) throw new Error("Expected mock kernel to be restarted after shutdown"); return OK_EXECUTION; }), ping: vi.fn(async () => alive), @@ -132,7 +129,7 @@ describe("AgentSession python cleanup", () => { it("does not dispose unrelated Python owners when createAgentSession fails before session construction", async () => { const { tempDir, cwd } = createTempProject(); tempDirs.push(tempDir); - const unrelatedKernel = createWarmupKernel(); + const unrelatedKernel = createMockKernel(); const unrelatedCwd = path.join(tempDir, "unrelated-before"); const throwingExtension: ExtensionFactory = () => { throw new Error("Extension init failed"); @@ -164,7 +161,6 @@ describe("AgentSession python cleanup", () => { slashCommands: [], enableMCP: false, enableLsp: false, - forcePythonWarmup: true, toolNames: ["eval"], }), ).rejects.toThrow("Extension init failed"); @@ -172,7 +168,7 @@ describe("AgentSession python cleanup", () => { expect(startSpy).toHaveBeenCalledTimes(1); expect(unrelatedKernel.shutdown).not.toHaveBeenCalled(); - const replacementKernel = createWarmupKernel(); + const replacementKernel = createMockKernel(); startSpy.mockResolvedValueOnce(replacementKernel as unknown as PythonKernelInstance); await pythonExecutor.executePython("print('fresh warmup before')", { cwd, @@ -198,7 +194,7 @@ describe("AgentSession python cleanup", () => { it("does not dispose unrelated Python owners when createAgentSession fails after session construction", async () => { const { tempDir, cwd } = createTempProject(); tempDirs.push(tempDir); - const unrelatedKernel = createWarmupKernel(); + const unrelatedKernel = createMockKernel(); const unrelatedCwd = path.join(tempDir, "unrelated-after"); vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true }); const startSpy = vi @@ -229,7 +225,6 @@ describe("AgentSession python cleanup", () => { slashCommands: [], enableMCP: false, enableLsp: false, - forcePythonWarmup: true, toolNames: ["eval"], }), ).rejects.toThrow("Memory startup failed"); @@ -237,7 +232,7 @@ describe("AgentSession python cleanup", () => { expect(startSpy).toHaveBeenCalledTimes(1); expect(unrelatedKernel.shutdown).not.toHaveBeenCalled(); - const replacementKernel = createWarmupKernel(); + const replacementKernel = createMockKernel(); startSpy.mockResolvedValueOnce(replacementKernel as unknown as PythonKernelInstance); await pythonExecutor.executePython("print('fresh warmup after')", { cwd, @@ -263,8 +258,6 @@ describe("AgentSession python cleanup", () => { it("waits for active SDK session Python work before releasing a shared retained kernel", async () => { const { tempDir, cwd } = createTempProject(); tempDirs.push(tempDir); - stubPythonWarmup(); - const kernel = new FakeKernel(); const blockedExecution = Promise.withResolvers(); const blockedExecutionStarted = Promise.withResolvers(); @@ -336,128 +329,9 @@ describe("AgentSession python cleanup", () => { expect(kernel.shutdownCalls).toBe(1); }); - - it("aborts tracked eval warmup during session dispose before executePython starts", async () => { - const { tempDir, cwd } = createTempProject(); - tempDirs.push(tempDir); - const blockedWarmupStarted = Promise.withResolvers(); - const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({ - output: "tool ok", - exitCode: 0, - cancelled: false, - truncated: false, - totalLines: 1, - totalBytes: 7, - outputLines: 1, - outputBytes: 7, - displayOutputs: [], - stdinRequested: false, - }); - let warmupCallCount = 0; - const warmupSpy = vi - .spyOn(pythonExecutor, "warmPythonEnvironment") - .mockImplementation(async (_cwd, _sessionId, _useSharedGateway, _sessionFile, _kernelOwnerId, signal) => { - warmupCallCount += 1; - if (warmupCallCount === 1) { - return { ok: true }; - } - blockedWarmupStarted.resolve(); - return await new Promise<{ ok: boolean; reason?: string }>(resolve => { - const onAbort = () => resolve({ ok: false, reason: "Warmup aborted" }); - if (signal?.aborted) { - onAbort(); - return; - } - signal?.addEventListener("abort", onAbort, { once: true }); - }); - }); - vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true }); - - const session = await createSession(tempDir, cwd); - const EvalTool = session.getToolByName("eval"); - expect(EvalTool).toBeDefined(); - let toolExecutionSettled = false; - const toolExecution = EvalTool! - .execute("call-id", { input: "```py\nprint('tool')\n```" }, undefined, undefined, undefined) - .finally(() => { - toolExecutionSettled = true; - }); - await blockedWarmupStarted.promise; - - let disposed = false; - const disposeSession = session.dispose().then(() => { - disposed = true; - }); - await Bun.sleep(0); - - expect(disposed).toBe(false); - expect(toolExecutionSettled).toBe(false); - expect(warmupSpy).toHaveBeenCalledTimes(2); - expect(executeSpy).not.toHaveBeenCalled(); - - await expect(toolExecution).rejects.toThrow("Operation aborted"); - await disposeSession; - - expect(disposed).toBe(true); - expect(toolExecutionSettled).toBe(true); - expect(executeSpy).not.toHaveBeenCalled(); - }); - - it("rejects eval starts when warmup finishes after dispose begins", async () => { - const { tempDir, cwd } = createTempProject(); - tempDirs.push(tempDir); - const blockedWarmupStarted = Promise.withResolvers(); - const releaseWarmup = Promise.withResolvers(); - const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({ - output: "late", - exitCode: 0, - cancelled: false, - truncated: false, - totalLines: 1, - totalBytes: 4, - outputLines: 1, - outputBytes: 4, - displayOutputs: [], - stdinRequested: false, - }); - let warmupCallCount = 0; - const warmupSpy = vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockImplementation(async () => { - warmupCallCount += 1; - if (warmupCallCount === 1) { - return { ok: true }; - } - blockedWarmupStarted.resolve(); - await releaseWarmup.promise; - return { ok: true }; - }); - vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true }); - - const session = await createSession(tempDir, cwd); - const EvalTool = session.getToolByName("eval"); - expect(EvalTool).toBeDefined(); - const toolExecution = EvalTool!.execute( - "call-id", - { input: "```py\nprint('tool after warmup')\n```" }, - undefined, - undefined, - undefined, - ); - await blockedWarmupStarted.promise; - const disposeSession = session.dispose(); - releaseWarmup.resolve(); - await expect(toolExecution).rejects.toThrow( - "Python execution is unavailable while session disposal is in progress", - ); - await disposeSession; - expect(warmupSpy).toHaveBeenCalledTimes(2); - expect(executeSpy).not.toHaveBeenCalled(); - }); it("aborts tracked eval execution during session dispose after warmup completes", async () => { const { tempDir, cwd } = createTempProject(); tempDirs.push(tempDir); - const warmupSpy = vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockImplementation(async () => { - return { ok: true }; - }); const blockedExecuteStarted = Promise.withResolvers(); const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockImplementation(async (_code, options) => { const signal = options?.signal; @@ -507,14 +381,12 @@ describe("AgentSession python cleanup", () => { expect(disposed).toBe(false); expect(toolExecutionSettled).toBe(false); - expect(warmupSpy).toHaveBeenCalledTimes(2); expect(executeSpy).toHaveBeenCalledTimes(1); const [toolResult] = await Promise.all([toolExecution, disposeSession]); expect(disposed).toBe(true); expect(toolExecutionSettled).toBe(true); - expect(warmupSpy).toHaveBeenCalledTimes(2); expect(executeSpy).toHaveBeenCalledTimes(1); expect(toolResult.details?.isError).toBe(true); expect(toolResult.content).toContainEqual( @@ -525,8 +397,6 @@ describe("AgentSession python cleanup", () => { it("detaches retained kernel ownership even when dispose times out waiting for Python work", async () => { const { tempDir, cwd } = createTempProject(); tempDirs.push(tempDir); - stubPythonWarmup(); - const kernel = new FakeKernel(); const blockedExecution = Promise.withResolvers(); const blockedExecutionStarted = Promise.withResolvers(); @@ -583,7 +453,6 @@ describe("AgentSession python cleanup", () => { it("rejects direct session Python starts once dispose begins", async () => { const { tempDir, cwd } = createTempProject(); tempDirs.push(tempDir); - stubPythonWarmup(); const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({ output: "late", exitCode: 0, @@ -705,7 +574,6 @@ describe("AgentSession python cleanup", () => { it("rejects eval starts once dispose begins", async () => { const { tempDir, cwd } = createTempProject(); tempDirs.push(tempDir); - const warmupSpy = stubPythonWarmup(); const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({ output: "late", exitCode: 0, @@ -724,23 +592,15 @@ describe("AgentSession python cleanup", () => { expect(EvalTool).toBeDefined(); const disposeSession = session.dispose(); await expect( - EvalTool!.execute( - "call-id", - { input: "```py\nprint('late')\n```" }, - undefined, - undefined, - undefined, - ), + EvalTool!.execute("call-id", { input: "```py\nprint('late')\n```" }, undefined, undefined, undefined), ).rejects.toThrow("Python execution is unavailable while session disposal is in progress"); await disposeSession; - expect(warmupSpy).toHaveBeenCalledTimes(1); expect(executeSpy).not.toHaveBeenCalled(); }); it("rejects eval starts that reach async preflight after dispose begins", async () => { const { tempDir, cwd } = createTempProject(); tempDirs.push(tempDir); - const warmupSpy = stubPythonWarmup(); const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({ output: "late", exitCode: 0, @@ -777,15 +637,12 @@ describe("AgentSession python cleanup", () => { releaseArtifact.resolve(); await expect(execution).rejects.toThrow("Python execution is unavailable while session disposal is in progress"); await disposeSession; - expect(warmupSpy).toHaveBeenCalledTimes(1); expect(executeSpy).not.toHaveBeenCalled(); }); it("aborts every active Python execution owned by the session during dispose", async () => { const { tempDir, cwd } = createTempProject(); tempDirs.push(tempDir); - stubPythonWarmup(); - const kernel = new FakeKernel(); const blockedExecution = Promise.withResolvers(); const blockedExecutionStarted = Promise.withResolvers(); diff --git a/packages/coding-agent/test/core/js-executor.test.ts b/packages/coding-agent/test/core/js-executor.test.ts index 566eb7deb..f8a35a390 100644 --- a/packages/coding-agent/test/core/js-executor.test.ts +++ b/packages/coding-agent/test/core/js-executor.test.ts @@ -120,13 +120,13 @@ describe("executeJs", () => { "const uuid = crypto.randomUUID();", "const digest = await webcrypto.subtle.digest('SHA-256', new TextEncoder().encode('ok'));", "const base = process.cwd();", - "fs.mkdirSync(`${base}/nested`, { recursive: true });", - "fs.writeFileSync(`${base}/nested/value.txt`, 'hello');", - "await fs.promises.copyFile(`${base}/nested/value.txt`, `${base}/nested/copy.txt`);", - "const text = fs.readFileSync(`${base}/nested/copy.txt`, 'utf8');", - "const bytes = await fs.promises.readFile(`${base}/nested/copy.txt`);", - "const stat = fs.statSync(`${base}/nested/copy.txt`);", - "const entries = fs.readdirSync(`${base}/nested`);", + "fs.mkdirSync(base + '/nested', { recursive: true });", + "fs.writeFileSync(base + '/nested/value.txt', 'hello');", + "await fs.promises.copyFile(base + '/nested/value.txt', base + '/nested/copy.txt');", + "const text = fs.readFileSync(base + '/nested/copy.txt', 'utf8');", + "const bytes = await fs.promises.readFile(base + '/nested/copy.txt');", + "const stat = fs.statSync(base + '/nested/copy.txt');", + "const entries = fs.readdirSync(base + '/nested');", "const start = performance.now();", "return {", " uuid: typeof uuid,", @@ -198,77 +198,6 @@ describe("executeJs", () => { expect(directoryResult.output).toContain("Directory paths are not supported"); }); - it("supports parse/stringify helpers and row utilities", async () => { - const result = await executeJs( - [ - 'const csv = \'name,role,note\\nalice,admin,"hello, world"\\nbob,user,"line1\\nline2"\';', - "const parsed = await parse.csv(csv);", - "const tsv = stringify.tsv(rows.pick(parsed, ['name', 'role']));", - "const roundtrip = await parse.tsv(tsv);", - "const grouped = rows.groupBy(parsed, 'role');", - "const counted = rows.countBy(parsed, 'role');", - "const sorted = rows.sortBy(parsed, 'name', { reverse: true });", - "return { parsed, roundtrip, groupedSizes: Object.fromEntries(Object.entries(grouped).map(([key, value]) => [key, value.length])), counted, sorted: rows.column(sorted, 'name') };", - ].join("\n"), - { - sessionId, - session, - sessionFile, - }, - ); - - expect(result.exitCode).toBe(0); - expect(result.displayOutputs).toEqual([ - { - type: "json", - data: { - parsed: [ - { name: "alice", role: "admin", note: "hello, world" }, - { name: "bob", role: "user", note: "line1\nline2" }, - ], - roundtrip: [ - { name: "alice", role: "admin" }, - { name: "bob", role: "user" }, - ], - groupedSizes: { admin: 1, user: 1 }, - counted: { admin: 1, user: 1 }, - sorted: ["bob", "alice"], - }, - }, - ]); - }); - - it("round-trips text and binary writes", async () => { - const result = await executeJs( - [ - "await write('plain.txt', 'hello');", - "await write('bin.dat', new Uint8Array([0, 1, 2, 255]));", - "await write.json('state.json', { ok: true });", - "await write.csv('table.csv', [{ name: 'alice', role: 'admin' }]);", - "return {", - " text: await read.text('plain.txt'),", - " bytes: Array.from(await read.bytes('bin.dat')),", - " json: await read.json('state.json'),", - " csv: await read.csv('table.csv'),", - "};", - ].join("\n"), - { - sessionId, - session, - sessionFile, - }, - ); - - expect(result.exitCode).toBe(0); - expect(getStatusEvents(result)).toHaveLength(8); - expect(getJsonData(result)).toEqual({ - text: "hello", - bytes: [0, 1, 2, 255], - json: { ok: true }, - csv: [{ name: "alice", role: "admin" }], - }); - }); - it("routes output() through tool.read and keeps tool.* results normalized", async () => { const execute = vi.fn(async (_toolCallId: string, args: unknown): Promise => { const record = args as { path: string }; diff --git a/packages/coding-agent/test/core/python-executor-owner-cleanup.test.ts b/packages/coding-agent/test/core/python-executor-owner-cleanup.test.ts index 963757468..1377ec0ee 100644 --- a/packages/coding-agent/test/core/python-executor-owner-cleanup.test.ts +++ b/packages/coding-agent/test/core/python-executor-owner-cleanup.test.ts @@ -3,7 +3,6 @@ import { disposeAllKernelSessions, disposeKernelSessionsByOwner, executePython, - warmPythonEnvironment, } from "@oh-my-pi/pi-coding-agent/eval/py/executor"; import * as gatewayCoordinator from "@oh-my-pi/pi-coding-agent/eval/py/gateway-coordinator"; import type { @@ -13,7 +12,6 @@ import type { } from "@oh-my-pi/pi-coding-agent/eval/py/kernel"; import * as pythonKernel from "@oh-my-pi/pi-coding-agent/eval/py/kernel"; import { PythonKernel } from "@oh-my-pi/pi-coding-agent/eval/py/kernel"; -import { TempDir } from "@oh-my-pi/pi-utils"; const OK_RESULT: KernelExecuteResult = { status: "ok", @@ -775,80 +773,6 @@ describe("python executor owner cleanup", () => { expect(globalKernel.shutdown).toHaveBeenCalledTimes(1); await globalCleanup; }); - - it("attaches cached warmup sessions to newly provided owners", async () => { - using tempDir = TempDir.createSync("@python-owner-warmup-"); - const kernel = { - execute: vi.fn(async () => OK_RESULT), - ping: vi.fn(async () => true), - isAlive: () => true, - shutdown: vi.fn(async (): Promise => ({ confirmed: true })), - }; - vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true }); - const startSpy = vi.spyOn(PythonKernel, "start").mockResolvedValue(kernel as unknown as PythonKernelInstance); - - const firstWarmup = await warmPythonEnvironment(tempDir.path(), "warm-session", true, undefined, "owner-a"); - expect(firstWarmup.ok).toBe(true); - - const cachedWarmup = await warmPythonEnvironment(tempDir.path(), "warm-session", true, undefined, "owner-b"); - expect(cachedWarmup.ok).toBe(true); - expect(startSpy).toHaveBeenCalledTimes(1); - - await disposeKernelSessionsByOwner("owner-a"); - expect(kernel.shutdown).not.toHaveBeenCalled(); - - await executePython("1 + 1", { - cwd: tempDir.path(), - sessionId: "warm-session", - kernelMode: "session", - kernelOwnerId: "owner-b", - }); - - expect(startSpy).toHaveBeenCalledTimes(1); - expect(kernel.execute).toHaveBeenCalledTimes(1); - - await disposeKernelSessionsByOwner("owner-b"); - expect(kernel.shutdown).toHaveBeenCalledTimes(1); - }); - - it("keeps cache-hit ownerless warmups provisional until an explicit owner takes over", async () => { - using tempDir = TempDir.createSync("@python-owner-fallback-"); - const kernel = { - execute: vi.fn(async () => OK_RESULT), - ping: vi.fn(async () => true), - isAlive: () => true, - shutdown: vi.fn(async (): Promise => ({ confirmed: true })), - }; - vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true }); - const startSpy = vi.spyOn(PythonKernel, "start").mockResolvedValue(kernel as unknown as PythonKernelInstance); - - const firstWarmup = await warmPythonEnvironment(tempDir.path(), "warm-fallback-session", true); - expect(firstWarmup.ok).toBe(true); - - const cachedWarmup = await warmPythonEnvironment(tempDir.path(), "warm-fallback-session", true); - expect(cachedWarmup.ok).toBe(true); - expect(startSpy).toHaveBeenCalledTimes(1); - - await executePython("1 + 1", { - cwd: tempDir.path(), - sessionId: "warm-fallback-session", - kernelMode: "session", - kernelOwnerId: "owner-a", - }); - - expect(kernel.execute).toHaveBeenCalledTimes(1); - - const postTakeoverWarmup = await warmPythonEnvironment(tempDir.path(), "warm-fallback-session", true); - expect(postTakeoverWarmup.ok).toBe(true); - expect(startSpy).toHaveBeenCalledTimes(1); - - await disposeKernelSessionsByOwner("owner-a"); - expect(kernel.shutdown).toHaveBeenCalledTimes(1); - - await disposeKernelSessionsByOwner("warm-fallback-session"); - expect(kernel.shutdown).toHaveBeenCalledTimes(1); - }); - it("leaves per-call kernels out of owner-scoped retained cleanup and keeps global cleanup intact", async () => { const perCallKernel = new FakeKernel(); const retainedKernel = new FakeKernel(); diff --git a/packages/coding-agent/test/core/python-prelude.test.ts b/packages/coding-agent/test/core/python-prelude.test.ts index 302b0d201..4082e48a9 100644 --- a/packages/coding-agent/test/core/python-prelude.test.ts +++ b/packages/coding-agent/test/core/python-prelude.test.ts @@ -2,8 +2,7 @@ import { describe, expect, it } from "bun:test"; import * as fs from "node:fs"; import * as path from "node:path"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; -import { warmPythonEnvironment } from "@oh-my-pi/pi-coding-agent/eval/py/executor"; -import { EvalTool, getEvalToolDescription } from "@oh-my-pi/pi-coding-agent/tools/eval"; +import { EvalTool } from "@oh-my-pi/pi-coding-agent/tools/eval"; import { $which, getProjectDir } from "@oh-my-pi/pi-utils"; const resolvePythonPath = (): string | null => { @@ -40,7 +39,22 @@ const shouldRun = Boolean(pythonPath) && hasKernelDeps; describe.skipIf(!shouldRun)("PYTHON_PRELUDE integration", () => { it("exposes prelude helpers via eval python backend", async () => { - const helpers = ["env", "read", "write", "append", "find", "glob", "grep", "rgrep", "sed", "tree", "stat", "diff", "run", "output"]; + const helpers = [ + "env", + "read", + "write", + "append", + "find", + "glob", + "grep", + "rgrep", + "sed", + "tree", + "stat", + "diff", + "run", + "output", + ]; const session = { cwd: getProjectDir(), @@ -74,12 +88,4 @@ ${code} expect(tool.description).toContain("read"); expect(tool.description).not.toContain("Documentation unavailable"); }); - - it("renders prelude docs in eval tool description", async () => { - const result = await warmPythonEnvironment(getProjectDir()); - expect(result.ok).toBe(true); - const description = getEvalToolDescription(); - expect(description).toContain("read"); - expect(description).not.toContain("Documentation unavailable"); - }); }); diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index 166f10dc8..0579c3540 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [14.5.13] - 2026-05-01 ### Changed - Stopped overriding `CARGO_TARGET_DIR` with an internal `target/napi-build/...` directory during native builds, so Cargo now uses the default or caller-provided target directory diff --git a/packages/natives/package.json b/packages/natives/package.json index 42a701c7f..854c3f8b1 100644 --- a/packages/natives/package.json +++ b/packages/natives/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-natives", - "version": "14.5.12", + "version": "14.5.13", "description": "Native Rust bindings for grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/stats/package.json b/packages/stats/package.json index 0f73ea703..c89939ddb 100644 --- a/packages/stats/package.json +++ b/packages/stats/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/omp-stats", - "version": "14.5.12", + "version": "14.5.13", "description": "Local observability dashboard for pi AI usage statistics", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/swarm-extension/package.json b/packages/swarm-extension/package.json index dc79c78ff..94aac3bad 100644 --- a/packages/swarm-extension/package.json +++ b/packages/swarm-extension/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/swarm-extension", - "version": "14.5.12", + "version": "14.5.13", "description": "Swarm orchestration extension for omp", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Derek Rynd", diff --git a/packages/tui/package.json b/packages/tui/package.json index ceee150bc..3a3edcfdd 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-tui", - "version": "14.5.12", + "version": "14.5.13", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/utils/package.json b/packages/utils/package.json index 6434019d7..d9e404506 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@oh-my-pi/pi-utils", - "version": "14.5.12", + "version": "14.5.13", "description": "Shared utilities for pi packages", "homepage": "https://github.com/can1357/oh-my-pi", "author": "Can Boluk", diff --git a/packages/utils/src/frontmatter.ts b/packages/utils/src/frontmatter.ts index 75cfba652..828ca3e44 100644 --- a/packages/utils/src/frontmatter.ts +++ b/packages/utils/src/frontmatter.ts @@ -8,23 +8,33 @@ function stripHtmlComments(content: string): string { /** Convert kebab-case to camelCase (e.g. "thinking-level" -> "thinkingLevel") */ function kebabToCamel(key: string): string { + if (!key.includes("-")) return key; return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); } /** Recursively normalize object keys from kebab-case to camelCase */ function normalizeKeys(obj: T): T { - if (obj === null || typeof obj !== "object") { - return obj; - } + if (obj === null || typeof obj !== "object") return obj; if (Array.isArray(obj)) { - return obj.map(normalizeKeys) as T; + let changed = false; + const out: unknown[] = new Array(obj.length); + for (let i = 0; i < obj.length; i++) { + const v = obj[i]; + const nv = normalizeKeys(v); + out[i] = nv; + if (nv !== v) changed = true; + } + return (changed ? (out as unknown) : obj) as T; } + let changed = false; const result: Record = {}; for (const [key, value] of Object.entries(obj as Record)) { - const normalizedKey = kebabToCamel(key); - result[normalizedKey] = normalizeKeys(value); + const nk = key.includes("-") ? kebabToCamel(key) : key; + const nv = normalizeKeys(value); + result[nk] = nv; + if (nk !== key || nv !== value) changed = true; } - return result as T; + return (changed ? result : obj) as T; } export class FrontmatterError extends Error { diff --git a/packages/utils/src/prompt.ts b/packages/utils/src/prompt.ts index b4802f7cf..4224bfd19 100644 --- a/packages/utils/src/prompt.ts +++ b/packages/utils/src/prompt.ts @@ -418,8 +418,17 @@ function disambiguateClosingBraces(template: string): string { return template.replace(/\}\}(\}+)/g, "}}{{!---}}$1"); } +const compiledTemplateCache = new Map string>(); + export function compile(template: string): (context: TemplateContext) => string { - return handlebars.compile(disambiguateClosingBraces(template), { noEscape: true, strict: false }); + const disambiguated = disambiguateClosingBraces(template); + const cached = compiledTemplateCache.get(disambiguated); + if (cached) return cached; + const compiled = handlebars.compile(disambiguated, { noEscape: true, strict: false }) as ( + context: TemplateContext, + ) => string; + compiledTemplateCache.set(disambiguated, compiled); + return compiled; } export function render(template: string, context: TemplateContext = {}): string {