diff --git a/.github/workflows/backend-test-windows.yml b/.github/workflows/backend-test-windows.yml index 96b2719737..f7c49654d1 100644 --- a/.github/workflows/backend-test-windows.yml +++ b/.github/workflows/backend-test-windows.yml @@ -98,6 +98,21 @@ jobs: vcpkg.exe install openssl:x64-windows-static vcpkg.exe integrate install + - name: Free disk space (post-vcpkg) + shell: pwsh + run: | + # vcpkg leaves multi-GB of buildtrees/downloads after installing openssl; + # we only need the installed/ dir for linking. + $vcpkgRoot = $env:VCPKG_INSTALLATION_ROOT + foreach ($sub in @("buildtrees", "downloads", "packages")) { + $path = Join-Path $vcpkgRoot $sub + if (Test-Path $path) { + Write-Host "Removing $path" + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $path + } + } + Get-PSDrive C | Select-Object Used,Free | Format-Table -AutoSize + - name: Get runtime paths id: runtime-paths shell: pwsh @@ -119,6 +134,10 @@ jobs: cargo build --release -p windmill_duckdb_ffi_internal New-Item -ItemType Directory -Path ..\target\debug -Force Copy-Item target\release\windmill_duckdb_ffi_internal.dll ..\target\debug\ + # duckdb is bundled (~2GB of build artifacts); the DLL is the only + # thing we need from this excluded-crate target dir. + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue target + Get-PSDrive C | Select-Object Used,Free | Format-Table -AutoSize - name: Print runtime versions and env shell: pwsh @@ -136,6 +155,10 @@ jobs: echo "USERPROFILE=$env:USERPROFILE" echo "HOME=$env:HOME" + - name: Disk space before cargo test + shell: pwsh + run: Get-PSDrive C | Select-Object Used,Free | Format-Table -AutoSize + - name: cargo test working-directory: backend timeout-minutes: 60 @@ -144,13 +167,16 @@ jobs: RUST_LOG: "off" RUST_LOG_STYLE: never CARGO_NET_GIT_FETCH_WITH_CLI: true - CARGO_BUILD_JOBS: 12 + # 16-vcpu runners with disabled PDB still hit LNK1180 ("insufficient + # disk space") at link time with 12 parallel link jobs: each test + # binary link spikes several hundred MB of transient I/O. Capping at + # 8 trades ~25% wall time for headroom on the ~75GB runner disk. + CARGO_BUILD_JOBS: 8 # backend/Cargo.toml sets split-debuginfo = "unpacked", which on # windows-msvc is coerced to "packed": every test-binary link spawns - # the mspdbsrv.exe PDB type server and writes a large .pdb. With 12 - # parallel link jobs this races the type-server cap (LNK1318 "LIMIT - # (12)") and exhausts the runner disk (LNK1180). CI needs no debug - # info, so disable PDB generation for the dev/test profiles here. + # the mspdbsrv.exe PDB type server and writes a large .pdb. CI needs + # no debug info, so disable PDB generation for the dev/test profiles + # here (avoids both LNK1318 type-server limit and PDB disk usage). CARGO_PROFILE_DEV_SPLIT_DEBUGINFO: "off" CARGO_PROFILE_TEST_SPLIT_DEBUGINFO: "off" # Tests' poll-time stack frames (deep nested async fn chains in diff --git a/CHANGELOG.md b/CHANGELOG.md index fffb413e6e..ebbf6cb922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [1.711.0](https://github.com/windmill-labs/windmill/compare/v1.710.1...v1.711.0) (2026-05-26) + + +### Features + +* **cli:** add object-storage commands and flow test-step ([#9326](https://github.com/windmill-labs/windmill/issues/9326)) ([36f574f](https://github.com/windmill-labs/windmill/commit/36f574ff951198a4d40ee068a27d74c41ce32154)) + + +### Bug Fixes + +* **cli:** handle __flow suffix when deriving the flow's Windmill path ([#9333](https://github.com/windmill-labs/windmill/issues/9333)) ([6f77034](https://github.com/windmill-labs/windmill/commit/6f770346fb330997a836c39fba347df4c088a83c)) +* **queue:** duration-weighted workspace fairness signal ([#9329](https://github.com/windmill-labs/windmill/issues/9329)) ([42d2121](https://github.com/windmill-labs/windmill/commit/42d2121af925de50f549ecb72ffb5132f5c41079)) + +## [1.710.1](https://github.com/windmill-labs/windmill/compare/v1.710.0...v1.710.1) (2026-05-26) + + +### Bug Fixes + +* improve workspace fairness ([896add0](https://github.com/windmill-labs/windmill/commit/896add0350f4de31f5674d6be0907a582c5ec17e)) + +## [1.710.0](https://github.com/windmill-labs/windmill/compare/v1.709.0...v1.710.0) (2026-05-26) + + +### Features + +* **queue:** stochastic admission + EE availability of workspace fairness algorithm ([#9321](https://github.com/windmill-labs/windmill/issues/9321)) ([8bf7fd2](https://github.com/windmill-labs/windmill/commit/8bf7fd2c921c48861b71731a085b18ea8f72fb68)) + + +### Bug Fixes + +* **websocket-trigger:** honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY ([#9324](https://github.com/windmill-labs/windmill/issues/9324)) ([6f36316](https://github.com/windmill-labs/windmill/commit/6f363163df9cd15f5af7d56cf34a01b70d236830)) + ## [1.709.0](https://github.com/windmill-labs/windmill/compare/v1.708.0...v1.709.0) (2026-05-25) diff --git a/backend/.sqlx/query-8597cd40f80e69edbf1bc7d7402baca32e33e871be454acb5175c11361fe1b0a.json b/backend/.sqlx/query-8597cd40f80e69edbf1bc7d7402baca32e33e871be454acb5175c11361fe1b0a.json new file mode 100644 index 0000000000..fd82867507 --- /dev/null +++ b/backend/.sqlx/query-8597cd40f80e69edbf1bc7d7402baca32e33e871be454acb5175c11361fe1b0a.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM background_task_state\n WHERE name LIKE $1\n AND updated_at < NOW() - INTERVAL '7 days'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "8597cd40f80e69edbf1bc7d7402baca32e33e871be454acb5175c11361fe1b0a" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index d154d1b2bf..c32e1d9c67 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -620,7 +620,7 @@ checksum = "850b60ddcc664dcd848f8a2fa8436ab9336e051d6dd2b3f21f897dd8e9c24703" dependencies = [ "base64 0.22.1", "bytes", - "http 1.4.0", + "http 1.4.1", "rand 0.8.5", "reqwest 0.12.28", "serde", @@ -752,7 +752,7 @@ dependencies = [ "bytes", "fastrand", "hex", - "http 1.4.0", + "http 1.4.1", "ring 0.17.14", "time", "tokio", @@ -813,7 +813,7 @@ dependencies = [ "bytes", "bytes-utils", "fastrand", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "percent-encoding", "pin-project-lite", @@ -840,7 +840,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] @@ -915,7 +915,7 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", "url", @@ -940,7 +940,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] @@ -986,7 +986,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] @@ -1010,7 +1010,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] @@ -1054,7 +1054,7 @@ dependencies = [ "hex", "hmac", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "percent-encoding", "sha2 0.10.9", "time", @@ -1097,7 +1097,7 @@ dependencies = [ "futures-core", "futures-util", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "http-body 0.4.6", "percent-encoding", "pin-project-lite", @@ -1117,7 +1117,7 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "percent-encoding", @@ -1138,7 +1138,7 @@ dependencies = [ "h2 0.3.27", "h2 0.4.14", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "http-body 0.4.6", "hyper 0.14.32", "hyper 1.9.0", @@ -1208,7 +1208,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -1228,7 +1228,7 @@ dependencies = [ "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "pin-project-lite", "tokio", "tracing", @@ -1246,7 +1246,7 @@ dependencies = [ "bytes-utils", "futures-core", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -1304,7 +1304,7 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "itoa", @@ -1332,7 +1332,7 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.9.0", @@ -1365,7 +1365,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "mime", @@ -1384,7 +1384,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "mime", @@ -1682,7 +1682,7 @@ dependencies = [ "futures-core", "futures-util", "hex", - "http 1.4.0", + "http 1.4.1", "http-body-util", "hyper 1.9.0", "hyper-named-pipe", @@ -3597,7 +3597,7 @@ dependencies = [ "error_reporter", "h2 0.4.14", "hickory-resolver", - "http 1.4.0", + "http 1.4.1", "http-body-util", "hyper 1.9.0", "hyper-rustls 0.27.9", @@ -5175,7 +5175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de13e62d7e0ffc3eb40a0113ddf753cf6ec741be739164442b08893db4f9bfca" dependencies = [ "google-cloud-token", - "http 1.4.0", + "http 1.4.1", "thiserror 1.0.69", "tokio", "tokio-retry2", @@ -5296,7 +5296,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.1", "indexmap 2.14.0", "slab", "tokio", @@ -5403,7 +5403,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http 1.4.0", + "http 1.4.1", "httpdate", "mime", "sha1", @@ -5415,7 +5415,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.4.0", + "http 1.4.1", ] [[package]] @@ -5450,7 +5450,7 @@ checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" dependencies = [ "dirs 6.0.0", "futures", - "http 1.4.0", + "http 1.4.1", "indicatif", "libc", "log", @@ -5584,9 +5584,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -5610,7 +5610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.1", ] [[package]] @@ -5621,7 +5621,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "pin-project-lite", ] @@ -5647,7 +5647,7 @@ dependencies = [ "async-compression", "bstr", "futures", - "http 1.4.0", + "http 1.4.1", "http-body-util", "hyper 1.9.0", "hyper-rustls 0.26.0", @@ -5708,7 +5708,7 @@ dependencies = [ "futures-channel", "futures-core", "h2 0.4.14", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "httparse", "httpdate", @@ -5728,7 +5728,7 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "hyper-rustls 0.27.9", "hyper-tls", @@ -5780,7 +5780,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "hyper-util", "log", @@ -5798,7 +5798,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "hyper-util", "log", @@ -5864,7 +5864,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "hyper 1.9.0", "ipnet", @@ -6403,7 +6403,7 @@ dependencies = [ "either", "futures", "home", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.9.0", @@ -6437,7 +6437,7 @@ dependencies = [ "chrono", "derive_more 2.1.1", "form_urlencoded", - "http 1.4.0", + "http 1.4.1", "json-patch", "k8s-openapi", "schemars 0.8.22", @@ -7177,7 +7177,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.4.0", + "http 1.4.1", "httparse", "memchr", "mime", @@ -7685,7 +7685,7 @@ dependencies = [ "base64 0.22.1", "chrono", "getrandom 0.2.17", - "http 1.4.0", + "http 1.4.1", "rand 0.8.5", "reqwest 0.12.28", "serde", @@ -7716,7 +7716,7 @@ dependencies = [ "chrono", "form_urlencoded", "futures", - "http 1.4.0", + "http 1.4.1", "http-body-util", "httparse", "humantime", @@ -7823,7 +7823,7 @@ dependencies = [ "dyn-clone", "ed25519-dalek", "hmac", - "http 1.4.0", + "http 1.4.1", "itertools 0.10.5", "log", "oauth2", @@ -7950,7 +7950,7 @@ checksum = "10a8a7f5f6ba7c1b286c2fbca0454eaba116f63bbe69ed250b642d36fbb04d80" dependencies = [ "async-trait", "bytes", - "http 1.4.0", + "http 1.4.1", "opentelemetry 0.27.1", ] @@ -7962,7 +7962,7 @@ checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" dependencies = [ "async-trait", "bytes", - "http 1.4.0", + "http 1.4.1", "opentelemetry 0.30.0", "reqwest 0.12.28", ] @@ -7975,7 +7975,7 @@ checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" dependencies = [ "async-trait", "futures-core", - "http 1.4.0", + "http 1.4.1", "opentelemetry 0.27.1", "opentelemetry-http 0.27.0", "opentelemetry-proto 0.27.0", @@ -7994,7 +7994,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" dependencies = [ - "http 1.4.0", + "http 1.4.1", "opentelemetry 0.30.0", "opentelemetry-http 0.30.0", "opentelemetry-proto 0.30.0", @@ -9423,7 +9423,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.14", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.9.0", @@ -9471,7 +9471,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.14", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.9.0", @@ -9512,7 +9512,7 @@ checksum = "07bc3f1384cffa4f274dad2d4ddd73aed32fed8f786d96c6be8aa4e5fd3c3b58" dependencies = [ "anyhow", "async-trait", - "http 1.4.0", + "http 1.4.1", "reqwest 0.13.1", "serde", "thiserror 2.0.18", @@ -9529,7 +9529,7 @@ dependencies = [ "async-trait", "futures", "getrandom 0.2.17", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "reqwest 0.13.1", "reqwest-middleware", @@ -9635,7 +9635,7 @@ dependencies = [ "bytes", "chrono", "futures", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "oauth2", @@ -12538,7 +12538,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.1", "httparse", "rand 0.8.5", "ring 0.17.14", @@ -12626,7 +12626,7 @@ dependencies = [ "bytes", "flate2", "h2 0.4.14", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.9.0", @@ -12658,7 +12658,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2 0.4.14", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper 1.9.0", @@ -12726,7 +12726,7 @@ dependencies = [ "axum-core 0.5.6", "cookie", "futures-util", - "http 1.4.0", + "http 1.4.1", "parking_lot", "pin-project-lite", "tower-layer", @@ -12745,7 +12745,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "mime", @@ -12980,7 +12980,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.1", "httparse", "log", "native-tls", @@ -13002,7 +13002,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.1", "httparse", "log", "native-tls", @@ -13683,7 +13683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "974fa1e325e6cc5327de8887f189a441fcff4f8eedcd31ec87f0ef0cc5283fbc" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.1", "thiserror 2.0.18", "url", ] @@ -13802,7 +13802,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-nats", @@ -13883,7 +13883,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.709.0" +version = "1.711.0" dependencies = [ "async-stream", "async-trait", @@ -13896,7 +13896,7 @@ dependencies = [ "bytes", "eventsource-stream", "futures", - "http 1.4.0", + "http 1.4.1", "lazy_static", "mime_guess", "reqwest 0.13.1", @@ -13916,7 +13916,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", @@ -13929,7 +13929,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "argon2", @@ -13958,7 +13958,7 @@ dependencies = [ "git-version", "hex", "hmac", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "indexmap 2.14.0", "itertools 0.14.0", @@ -14067,11 +14067,11 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "lazy_static", "quick_cache", @@ -14090,7 +14090,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14103,12 +14103,12 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "axum 0.8.9", "chrono", - "http 1.4.0", + "http 1.4.1", "itertools 0.14.0", "jsonwebtoken 8.3.0", "lazy_static", @@ -14129,7 +14129,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.709.0" +version = "1.711.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -14139,7 +14139,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14156,7 +14156,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "base64 0.22.1", @@ -14178,7 +14178,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -14201,7 +14201,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14217,7 +14217,7 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14238,7 +14238,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14259,7 +14259,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14273,7 +14273,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-nats", @@ -14305,13 +14305,13 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "axum 0.8.9", "base64 0.22.1", "chrono", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "lazy_static", "serde", @@ -14330,7 +14330,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "flate2", @@ -14348,11 +14348,11 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "axum 0.8.9", - "http 1.4.0", + "http 1.4.1", "indexmap 2.14.0", "itertools 0.14.0", "lazy_static", @@ -14370,7 +14370,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14390,12 +14390,12 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", "futures", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "itertools 0.14.0", "lazy_static", @@ -14420,7 +14420,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -14448,7 +14448,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.709.0" +version = "1.711.0" dependencies = [ "lazy_static", "serde", @@ -14460,13 +14460,13 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.709.0" +version = "1.711.0" dependencies = [ "argon2", "axum 0.8.9", "chrono", "dashmap", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "lazy_static", "serde", @@ -14485,7 +14485,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14499,12 +14499,12 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.709.0" +version = "1.711.0" dependencies = [ "axum 0.8.9", "chrono", "hex", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "lazy_static", "magic-crypt", @@ -14532,7 +14532,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.709.0" +version = "1.711.0" dependencies = [ "chrono", "lazy_static", @@ -14546,7 +14546,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -14565,7 +14565,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.709.0" +version = "1.711.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -14666,7 +14666,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.709.0" +version = "1.711.0" dependencies = [ "chrono", "itertools 0.14.0", @@ -14685,7 +14685,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.709.0" +version = "1.711.0" dependencies = [ "regex", "serde", @@ -14700,7 +14700,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -14724,7 +14724,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "futures", @@ -14741,7 +14741,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.709.0" +version = "1.711.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -14757,13 +14757,13 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", "chrono", "futures", - "http 1.4.0", + "http 1.4.1", "oauth2", "reqwest 0.12.28", "rmcp", @@ -14778,7 +14778,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -14787,7 +14787,7 @@ dependencies = [ "base64 0.22.1", "chrono", "hmac", - "http 1.4.0", + "http 1.4.1", "itertools 0.14.0", "lazy_static", "reqwest 0.13.1", @@ -14809,7 +14809,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "arc-swap", @@ -14834,7 +14834,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-stream", @@ -14868,7 +14868,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "futures", @@ -14886,7 +14886,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.709.0" +version = "1.711.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -14895,7 +14895,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -14907,7 +14907,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde_json", @@ -14919,7 +14919,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "gosyn", @@ -14931,7 +14931,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -14943,7 +14943,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde_json", @@ -14955,7 +14955,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "nu-parser", @@ -14966,7 +14966,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14977,7 +14977,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14989,7 +14989,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "rustpython-ast", @@ -15000,7 +15000,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-recursion", @@ -15022,7 +15022,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde_json", @@ -15034,7 +15034,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -15048,7 +15048,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15065,7 +15065,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -15078,7 +15078,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde", @@ -15090,7 +15090,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -15108,7 +15108,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -15124,7 +15124,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "rustpython-ast", @@ -15140,7 +15140,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde", @@ -15151,7 +15151,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-recursion", @@ -15189,7 +15189,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "const_format", @@ -15227,7 +15227,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.709.0" +version = "1.711.0" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -15238,14 +15238,14 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-recursion", "axum 0.8.9", "chrono", "futures", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "lazy_static", "quick_cache", @@ -15268,7 +15268,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -15292,13 +15292,13 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", "axum 0.8.9", "chrono", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "itertools 0.14.0", "lazy_static", @@ -15325,7 +15325,7 @@ dependencies = [ [[package]] name = "windmill-trigger-azure" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -15335,7 +15335,7 @@ dependencies = [ "chrono", "constant_time_eq 0.3.1", "hex", - "http 1.4.0", + "http 1.4.1", "itertools 0.14.0", "lazy_static", "quick_cache", @@ -15358,7 +15358,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -15378,7 +15378,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -15388,7 +15388,7 @@ dependencies = [ "chrono", "google-cloud-googleapis", "google-cloud-pubsub", - "http 1.4.0", + "http 1.4.1", "itertools 0.14.0", "jsonwebtoken 8.3.0", "lazy_static", @@ -15412,7 +15412,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -15423,7 +15423,7 @@ dependencies = [ "futures", "hex", "hmac", - "http 1.4.0", + "http 1.4.1", "hyper 1.9.0", "itertools 0.14.0", "lazy_static", @@ -15448,7 +15448,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -15471,7 +15471,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -15495,7 +15495,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-nats", @@ -15519,7 +15519,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -15554,7 +15554,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", @@ -15582,14 +15582,14 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-trait", "axum 0.8.9", "base64 0.22.1", "futures", - "http 1.4.0", + "http 1.4.1", "itertools 0.14.0", "serde", "serde_json", @@ -15607,7 +15607,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "bitflags 2.11.1", @@ -15626,7 +15626,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-once-cell", @@ -15736,7 +15736,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.709.0" +version = "1.711.0" dependencies = [ "bytes", "futures", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 6e1898ea45..e296f9fb6a 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.709.0" +version = "1.711.0" authors.workspace = true edition.workspace = true @@ -87,7 +87,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.709.0" +version = "1.711.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 90cf163875..3c4cbb02ae 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -2de9dc793360764dc81b9593d72cb50347656a52 +327d23f7438968a21bac9fd42e7f6f027c61477c diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index af99dcf776..f06ab454a3 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.lock +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -6183,7 +6183,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill-common" -version = "1.709.0" +version = "1.711.0" dependencies = [ "aho-corasick", "anyhow", @@ -6263,7 +6263,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.709.0" +version = "1.711.0" dependencies = [ "proc-macro2", "quote", @@ -6275,7 +6275,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.709.0" +version = "1.711.0" dependencies = [ "convert_case", "serde", @@ -6284,7 +6284,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -6296,7 +6296,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde_json", @@ -6308,7 +6308,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "gosyn", @@ -6320,7 +6320,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -6332,7 +6332,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde_json", @@ -6344,7 +6344,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "nu-parser", @@ -6355,7 +6355,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6366,7 +6366,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6389,7 +6389,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "async-recursion", @@ -6411,7 +6411,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde_json", @@ -6423,7 +6423,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -6437,7 +6437,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "convert_case", @@ -6454,7 +6454,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -6467,7 +6467,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "lazy_static", @@ -6497,7 +6497,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6513,7 +6513,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6529,7 +6529,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6561,7 +6561,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "serde", @@ -6572,7 +6572,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.709.0" +version = "1.711.0" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index 8b6f194448..f49ea30ecd 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.toml +++ b/backend/parsers/windmill-parser-wasm/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" members = ["."] [workspace.package] -version = "1.709.0" +version = "1.711.0" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 0c6e627718..f902a72826 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -2709,6 +2709,26 @@ pub async fn monitor_db( } }; + // run every hour (120 iterations * 30s = 3600s) + let cleanup_stale_server_heartbeats_f = async { + if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(120) { + if let Some(db) = conn.as_sql() { + match windmill_api::cleanup_stale_server_heartbeats(db).await { + Ok(count) if count > 0 => { + tracing::info!( + "Deleted {} stale server_heartbeat background_task_state rows", + count + ); + } + Err(e) => { + tracing::error!("Error cleaning up stale server_heartbeat rows: {:?}", e); + } + _ => {} + } + } + } + }; + // run every hour (120 iterations * 30s = 3600s) let manage_audit_partitions_f = async { if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(120) { @@ -2767,6 +2787,7 @@ pub async fn monitor_db( native_triggers_sync_f, cleanup_notify_events_f, check_expiring_tokens_f, + cleanup_stale_server_heartbeats_f, manage_audit_partitions_f, export_audit_logs_to_object_store_f, cleanup_scheduled_job_deletions_f, diff --git a/backend/tests/workspace_fairness.rs b/backend/tests/workspace_fairness.rs index 62cc86cff3..f5922037e4 100644 --- a/backend/tests/workspace_fairness.rs +++ b/backend/tests/workspace_fairness.rs @@ -79,15 +79,28 @@ async fn create_workspace(db: &Pool, id: &str) { .unwrap(); } +/// Insert `n` completed jobs for `workspace_id`, each ending `secs_ago` +/// seconds in the past with a 1-second wall-clock duration. The fairness +/// algorithm weights contributions by `duration_ms` (clamped to the window), +/// so each job contributes ~1 worker-second when fully inside the window. async fn insert_completed(db: &Pool, workspace_id: &str, n: usize, secs_ago: i32) { for _ in 0..n { + let id: Uuid = sqlx::query_scalar( + "INSERT INTO v2_job (id, workspace_id, kind) + VALUES (gen_random_uuid(), $1, 'script'::job_kind) RETURNING id", + ) + .bind(workspace_id) + .fetch_one(db) + .await + .unwrap(); sqlx::query( "INSERT INTO v2_job_completed (id, workspace_id, duration_ms, status, started_at, completed_at) - VALUES (gen_random_uuid(), $1, 1, 'success'::job_status, - NOW() - make_interval(secs => $2::int), - NOW() - make_interval(secs => $2::int))", + VALUES ($1, $2, 1000, 'success'::job_status, + NOW() - make_interval(secs => ($3::int + 1)), + NOW() - make_interval(secs => $3::int))", ) + .bind(id) .bind(workspace_id) .bind(secs_ago) .execute(db) @@ -106,12 +119,98 @@ async fn insert_queued( let mut ids = Vec::with_capacity(n); for _ in 0..n { let id: Uuid = sqlx::query_scalar( - "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, tag) - VALUES (gen_random_uuid(), $1, NOW(), $2, $3) RETURNING id", + "INSERT INTO v2_job (id, workspace_id, kind, tag) + VALUES (gen_random_uuid(), $1, 'script'::job_kind, $2) RETURNING id", ) .bind(workspace_id) + .bind(tag) + .fetch_one(db) + .await + .unwrap(); + // Running jobs need a `started_at` for the fairness algorithm to + // compute a positive elapsed-time contribution. Backdate by 1s so + // each running row contributes ~1 worker-second by the time the + // refresh runs, matching the `insert_completed` scale. + sqlx::query( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, tag, started_at) + VALUES ($1, $2, NOW(), $3, $4, + CASE WHEN $3 THEN NOW() - interval '1 second' ELSE NULL END)", + ) + .bind(id) + .bind(workspace_id) .bind(running) .bind(tag) + .execute(db) + .await + .unwrap(); + if running { + // The fairness algorithm bounds the running contribution by the + // per-job `v2_job_runtime.ping`. Insert a fresh ping so each + // running row accrues real-time worker-seconds. + sqlx::query( + "INSERT INTO v2_job_runtime (id, ping) VALUES ($1, NOW()) + ON CONFLICT (id) DO UPDATE SET ping = NOW()", + ) + .bind(id) + .execute(db) + .await + .unwrap(); + insert_live_worker_ping(db, workspace_id, id).await; + } + ids.push(id); + } + ids +} + +/// Insert a live `worker_ping` row claiming the given job. Each insert uses +/// a fresh randomly-named worker so callers can stack multiple pings without +/// PK collisions on `worker`. +async fn insert_live_worker_ping(db: &Pool, workspace_id: &str, job_id: Uuid) { + let worker_name = format!("test-worker-{}", Uuid::new_v4()); + sqlx::query( + "INSERT INTO worker_ping (worker, worker_instance, ping_at, ip, current_job_id, current_job_workspace_id) + VALUES ($1, 'test', NOW(), '127.0.0.1', $2, $3)", + ) + .bind(&worker_name) + .bind(job_id) + .bind(workspace_id) + .execute(db) + .await + .unwrap(); +} + +/// Insert a "zombie" running row: a row in `v2_job_queue` with `running=true` +/// but **no** live `worker_ping` claiming it (no paired worker, or the worker +/// has stopped pinging). The fairness algorithm must NOT count these — they +/// don't consume any worker slot. +async fn insert_zombie_running(db: &Pool, workspace_id: &str, n: usize) -> Vec { + let mut ids = Vec::with_capacity(n); + for _ in 0..n { + let id: Uuid = sqlx::query_scalar( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, tag, started_at) + VALUES (gen_random_uuid(), $1, NOW() - interval '1 hour', true, 'deno', + NOW() - interval '1 hour') RETURNING id", + ) + .bind(workspace_id) + .fetch_one(db) + .await + .unwrap(); + ids.push(id); + } + ids +} + +/// Insert a concurrency-suspended row: `running=true` AND `suspend > 0`. These +/// rows are not being processed by any worker (the flow is paused), so the +/// algorithm must not count them as slot occupancy. +async fn insert_suspended_running(db: &Pool, workspace_id: &str, n: usize) -> Vec { + let mut ids = Vec::with_capacity(n); + for _ in 0..n { + let id: Uuid = sqlx::query_scalar( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, suspend, tag) + VALUES (gen_random_uuid(), $1, NOW(), true, 1, 'deno') RETURNING id", + ) + .bind(workspace_id) .fetch_one(db) .await .unwrap(); @@ -334,6 +433,76 @@ async fn fairness_catches_slot_hoggers(db: Pool) { assert_eq!(overloaded_set(), vec!["hogger".to_string()]); } +/// Regression: a workspace with a large backlog of `running = true` rows that +/// have **no live worker** claiming them (worker died, ping went stale, etc.) +/// must not be counted as "active". A previous version of the algorithm +/// counted `v2_job_queue.running = true` directly and was perpetually pinned +/// on the workspace with the most zombie rows, masking every other workspace. +#[sqlx::test(fixtures("base"))] +#[serial] +#[ignore = "flaky in CI"] +async fn fairness_ignores_zombie_running_rows(db: Pool) { + reset_fairness_state(); + create_workspace(&db, "stuck_backlog").await; + create_workspace(&db, "real_noisy").await; + create_workspace(&db, "victim").await; + + // 100 zombie running rows for `stuck_backlog`. No paired worker_ping ⇒ + // no live worker is processing them. Old algorithm: 100 units of fake + // activity. New algorithm: 0 units. + insert_zombie_running(&db, "stuck_backlog", 100).await; + // `real_noisy` is genuinely flooding the cluster. + insert_completed(&db, "real_noisy", 60, 2).await; + insert_completed(&db, "victim", 5, 3).await; + + refresh_overloaded(&db).await.expect("refresh ok"); + + let set = overloaded_set(); + assert!( + !set.contains(&"stuck_backlog".to_string()), + "zombie running rows must not flag a workspace as overloaded; got {set:?}" + ); + assert_eq!( + set, + vec!["real_noisy".to_string()], + "the actually noisy workspace must surface even when another workspace \ + has a large backlog of zombie running rows; got {set:?}" + ); +} + +/// Regression: concurrency-suspended rows (`running = true AND suspend > 0`) +/// are not consuming worker slots — the flow is paused at a suspend step — +/// and must not contribute to the activity share. +#[sqlx::test(fixtures("base"))] +#[serial] +#[ignore = "flaky in CI"] +async fn fairness_ignores_concurrency_suspended_rows(db: Pool) { + reset_fairness_state(); + create_workspace(&db, "concurrency_capped").await; + create_workspace(&db, "real_noisy").await; + create_workspace(&db, "victim").await; + + // 100 concurrency-suspended rows. Each has `running = true` (the legacy + // signal) but `suspend > 0` (not actually on a worker). + insert_suspended_running(&db, "concurrency_capped", 100).await; + insert_completed(&db, "real_noisy", 60, 2).await; + insert_completed(&db, "victim", 5, 3).await; + + refresh_overloaded(&db).await.expect("refresh ok"); + + let set = overloaded_set(); + assert!( + !set.contains(&"concurrency_capped".to_string()), + "concurrency-suspended rows must not flag a workspace as overloaded; got {set:?}" + ); + assert_eq!( + set, + vec!["real_noisy".to_string()], + "noisy workspace must still surface despite another workspace's large \ + suspended backlog; got {set:?}" + ); +} + // --------------------------------------------------------------------------- // Simulation test // --------------------------------------------------------------------------- @@ -449,7 +618,21 @@ async fn mock_worker( stats: Arc>, completed_counter: Arc, ) { - let _ = worker_id; + let worker_name = format!("mock-worker-{worker_id}"); + // Each mock worker maintains its own `worker_ping` row, the way a real + // worker would: `current_job_*` set on pick-up, cleared on completion. + // The fairness algorithm now reads slot occupancy from `worker_ping` (so + // that concurrency-suspended rows and zombies with no live ping do not + // inflate the denominator), so the simulation must keep this in sync. + sqlx::query( + "INSERT INTO worker_ping (worker, worker_instance, ping_at) VALUES ($1, 'sim', NOW()) + ON CONFLICT (worker) DO UPDATE SET ping_at = NOW(), + current_job_id = NULL, current_job_workspace_id = NULL", + ) + .bind(&worker_name) + .execute(&db) + .await + .unwrap(); let standard_sql = "WITH picked AS ( SELECT id FROM v2_job_queue WHERE running = false AND scheduled_for <= now() @@ -516,6 +699,20 @@ async fn mock_worker( match row { Some((id, ws, dur_ms, created_at)) => { + // Claim the slot on this worker's ping so the fairness + // algorithm counts this workspace's slot occupancy. + sqlx::query( + "UPDATE worker_ping SET ping_at = NOW(), + current_job_id = $1, current_job_workspace_id = $2 + WHERE worker = $3", + ) + .bind(id) + .bind(&ws) + .bind(&worker_name) + .execute(&db) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(dur_ms as u64)).await; // Move to completed atomically: insert + delete in one query. @@ -534,6 +731,17 @@ async fn mock_worker( .await .unwrap(); + // Release the slot. + sqlx::query( + "UPDATE worker_ping SET ping_at = NOW(), + current_job_id = NULL, current_job_workspace_id = NULL + WHERE worker = $1", + ) + .bind(&worker_name) + .execute(&db) + .await + .unwrap(); + let latency_ms = (completed_at - created_at).num_milliseconds().max(0) as u64; { let mut s = stats.lock().await; @@ -543,7 +751,13 @@ async fn mock_worker( } None => { // Empty queue (or every queued workspace is capped). Back off - // briefly so we don't hammer the DB. + // briefly so we don't hammer the DB. Refresh the heartbeat so + // this worker's ping doesn't go stale during long idle gaps. + sqlx::query("UPDATE worker_ping SET ping_at = NOW() WHERE worker = $1") + .bind(&worker_name) + .execute(&db) + .await + .unwrap(); tokio::time::sleep(Duration::from_millis(2)).await; } } diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0ff12ef535..fdec53098c 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.709.0 + version: 1.711.0 title: Windmill API contact: diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 8b2b9d8128..41872d0353 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -1261,3 +1261,26 @@ pub async fn check_any_server_started(db: &DB, not_before: chrono::DateTime not_before` (the moment a restart was initiated), +/// so rows older than the cutoff cannot influence any restart decision and +/// are safe to delete. +pub async fn cleanup_stale_server_heartbeats(db: &DB) -> anyhow::Result { + let prefix = format!("{SERVER_HEARTBEAT_TASK}:"); + let res = sqlx::query!( + "DELETE FROM background_task_state + WHERE name LIKE $1 + AND updated_at < NOW() - INTERVAL '7 days'", + format!("{prefix}%"), + ) + .execute(db) + .await?; + Ok(res.rows_affected()) +} diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index fee997975f..deb8f1cf66 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.709.0"; +export const VERSION = "v1.711.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index 03a8d14d51..4717711ae5 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -4,7 +4,7 @@ import { Command } from "@cliffy/command"; import { Confirm } from "@cliffy/prompt/confirm"; import { Table } from "@cliffy/table"; import * as log from "../../core/log.ts"; -import { sep as SEP } from "node:path"; +import { dirname, sep as SEP } from "node:path"; import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "../../utils/yaml.ts"; import { readTextFile, validateRequiredArgs } from "../../utils/utils.ts"; @@ -542,6 +542,7 @@ async function preview( data?: string; silent: boolean; remote?: boolean; + step?: string; } & SyncOptions, flowPath: string ) { @@ -562,7 +563,10 @@ async function preview( if (!isFlowDir) { // Check if it's a flow.yaml file if (flowPath.endsWith("flow.yaml") || flowPath.endsWith("flow.json")) { - flowPath = flowPath.substring(0, flowPath.lastIndexOf(SEP)); + // Use dirname so a bare "flow.yaml" (no parent dir) becomes "." + // instead of "" — the latter, after appending SEP below, becomes "/" + // and silently reads from filesystem root. + flowPath = dirname(flowPath); } else { throw new Error( "Flow path must be a .flow/__flow directory or a flow.yaml file" @@ -636,18 +640,35 @@ async function preview( const input = opts.data ? await resolve(opts.data) : {}; + log.debug(`Flow value: ${JSON.stringify(localFlow.value, null, 2)}`); + + // Single-step mode: run only the named module's runnable. + // The full-flow prep above (inline-script replacement, local PathScript + // substitution, tempScriptRefs build) is exactly what the single step needs + // too — PathScript modules have already been rewritten to inline rawscript + // when `useLocalPathScripts` is set, and tempScriptRefs covers relative + // imports in inline scripts. + // Compute the flow's windmill path (e.g. "f/cli_smoke/myrelflow"). Used as + // the anchor for relative-import resolution: inline scripts in this flow are + // treated as living at "/", so "./util" resolves to + // "/util" — matching the keys in temp_script_refs. + const flowWmPath = stripFlowSuffix(flowPath).replaceAll(SEP, "/"); + + if (opts.step) { + await previewStep(opts.step, localFlow, flowWmPath, workspace, input, tempScriptRefs, opts.silent); + return; + } + if (!opts.silent) { log.info(colors.yellow(`Running flow preview for ${flowPath}...`)); } - log.debug(`Flow value: ${JSON.stringify(localFlow.value, null, 2)}`); - // Run the flow preview — start the job, then poll for completion const jobId = await wmill.runFlowPreview({ workspace: workspace.workspaceId, requestBody: { value: localFlow.value, - path: flowPath.substring(0, flowPath.indexOf(".flow")).replaceAll(SEP, "/"), + path: flowWmPath, args: input, temp_script_refs: tempScriptRefs, }, @@ -674,6 +695,176 @@ async function preview( } } +async function previewStep( + stepId: string, + localFlow: FlowFile, + flowWmPath: string, + workspace: { workspaceId: string }, + baseArgs: Record, + tempScriptRefs: Record | undefined, + silent: boolean, +) { + const module = findStepInFlowValue(localFlow.value, stepId); + if (!module) { + const available = collectStepIds(localFlow.value).join(", ") || "(none)"; + throw new Error(`Step '${stepId}' not found in flow. Available steps: ${available}`); + } + + // The preprocessor module receives args via _ENTRYPOINT_OVERRIDE so the + // runner picks the preprocessor entrypoint (matches frontend behavior in + // copilot/chat/flow/core.ts). + const args = + stepId === "preprocessor" + ? { _ENTRYPOINT_OVERRIDE: "preprocessor", ...baseArgs } + : baseArgs; + + const moduleValue = module.value; + let jobId: string; + if (moduleValue?.type === "rawscript") { + log.info(colors.yellow(`Previewing step '${stepId}' (rawscript, ${moduleValue.language})...`)); + jobId = await wmill.runScriptPreview({ + workspace: workspace.workspaceId, + requestBody: { + content: moduleValue.content ?? "", + language: moduleValue.language, + // Anchor relative imports to "/" so + // temp_script_refs (keyed by Windmill paths) resolve correctly. + // Without `path`, the worker defaults to "tmp/main" and "../foo" + // resolves to "tmp/foo", missing every entry in temp_script_refs. + path: `${flowWmPath}/${stepId}`, + flow_path: flowWmPath, + args, + temp_script_refs: tempScriptRefs, + }, + }); + } else if (moduleValue?.type === "script") { + // Falls through here only when the deployed PathScript is what we want — + // either --remote was passed, or no local file exists for this path. + log.info(colors.yellow(`Previewing step '${stepId}' (script ${moduleValue.path})...`)); + const script = moduleValue.hash + ? await wmill.getScriptByHash({ + workspace: workspace.workspaceId, + hash: moduleValue.hash, + }) + : await wmill.getScriptByPath({ + workspace: workspace.workspaceId, + path: moduleValue.path, + }); + jobId = await wmill.runScriptPreview({ + workspace: workspace.workspaceId, + requestBody: { + content: script.content, + language: script.language as any, + // Anchor to the script's own deployed path so its relative imports + // resolve against the workspace tree (or temp_script_refs). + path: moduleValue.path, + flow_path: flowWmPath, + args, + temp_script_refs: tempScriptRefs, + }, + }); + } else if (moduleValue?.type === "flow") { + log.info(colors.yellow(`Previewing step '${stepId}' (flow ${moduleValue.path})...`)); + jobId = await wmill.runFlowByPath({ + workspace: workspace.workspaceId, + path: moduleValue.path, + requestBody: args, + }); + } else { + throw new Error( + `Cannot preview step of type '${moduleValue?.type ?? "unknown"}'. Supported types: rawscript, script, flow.` + ); + } + + const { result, success } = await pollForJobResult(workspace.workspaceId, jobId); + + if (!success) { + if (silent) { + console.log(JSON.stringify(result)); + } else { + log.info(colors.red.bold(`Step '${stepId}' failed:`)); + log.info(JSON.stringify(result, null, 2)); + } + process.exitCode = 1; + return; + } + + if (silent) { + console.log(JSON.stringify(result)); + } else { + log.info(colors.bold.underline.green(`Step '${stepId}' completed`)); + log.info(JSON.stringify(result, null, 2)); + } +} + +// Strip the `.flow`/`__flow` directory suffix to recover the flow's logical +// Windmill path. Workspaces with nonDottedPaths use `__flow`; the default +// uses `.flow`. A previous version used `indexOf(".flow")` which returned -1 +// (and thus `substring(0, -1) === ""`) for `__flow` folders and for the +// `dirname("flow.yaml") === "."` fallback — producing an empty path that +// broke relative-import resolution downstream. +function stripFlowSuffix(flowPath: string): string { + const stripped = flowPath.endsWith(SEP) ? flowPath.slice(0, -SEP.length) : flowPath; + if (stripped.endsWith(".flow")) return stripped.slice(0, -".flow".length); + if (stripped.endsWith("__flow")) return stripped.slice(0, -"__flow".length); + return stripped; +} + +function findStepInFlowValue(flowValue: any, stepId: string): any | undefined { + if (!flowValue) return undefined; + if (flowValue.failure_module?.id === stepId) return flowValue.failure_module; + if (flowValue.preprocessor_module?.id === stepId) return flowValue.preprocessor_module; + return findStepInModules(flowValue.modules ?? [], stepId); +} + +function findStepInModules(modules: any[], stepId: string): any | undefined { + for (const m of modules) { + if (m?.id === stepId) return m; + const v = m?.value; + if (!v) continue; + if (v.type === "forloopflow" || v.type === "whileloopflow") { + const found = findStepInModules(v.modules ?? [], stepId); + if (found) return found; + } else if (v.type === "branchone") { + for (const b of v.branches ?? []) { + const found = findStepInModules(b.modules ?? [], stepId); + if (found) return found; + } + const found = findStepInModules(v.default ?? [], stepId); + if (found) return found; + } else if (v.type === "branchall") { + for (const b of v.branches ?? []) { + const found = findStepInModules(b.modules ?? [], stepId); + if (found) return found; + } + } + } + return undefined; +} + +function collectStepIds(flowValue: any): string[] { + const ids: string[] = []; + const walkModules = (modules: any[]) => { + for (const m of modules) { + if (m?.id) ids.push(m.id); + const v = m?.value; + if (!v) continue; + if (v.type === "forloopflow" || v.type === "whileloopflow") { + walkModules(v.modules ?? []); + } else if (v.type === "branchone") { + for (const b of v.branches ?? []) walkModules(b.modules ?? []); + walkModules(v.default ?? []); + } else if (v.type === "branchall") { + for (const b of v.branches ?? []) walkModules(b.modules ?? []); + } + } + }; + if (flowValue?.preprocessor_module?.id) ids.push(flowValue.preprocessor_module.id); + if (flowValue?.failure_module?.id) ids.push(flowValue.failure_module.id); + walkModules(flowValue?.modules ?? []); + return ids; +} + export async function generateLocks( opts: GlobalOptions & { yes?: boolean; @@ -890,7 +1081,7 @@ const command = new Command() .action(run as any) .command( "preview", - "preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default." + "preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow)." ) .arguments("") .option( @@ -905,6 +1096,10 @@ const command = new Command() "--remote", "Use deployed workspace scripts for PathScript steps instead of local files." ) + .option( + "--step ", + "Run only the named step instead of the whole flow. Honors --data as the step's args and --remote / local-PathScript resolution the same way the full-flow preview does." + ) .action(preview as any) .command( "generate-locks", diff --git a/cli/src/commands/object-storage/object-storage.ts b/cli/src/commands/object-storage/object-storage.ts new file mode 100644 index 0000000000..2ce404e292 --- /dev/null +++ b/cli/src/commands/object-storage/object-storage.ts @@ -0,0 +1,344 @@ +import { Buffer } from "node:buffer"; +import { readFile, writeFile } from "node:fs/promises"; +import { basename } from "node:path"; + +import { GlobalOptions } from "../../types.ts"; +import { requireLogin } from "../../core/auth.ts"; +import { resolveWorkspace } from "../../core/context.ts"; +import { Command } from "@cliffy/command"; +import { Confirm } from "@cliffy/prompt/confirm"; +import { Table } from "@cliffy/table"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "../../core/log.ts"; +import * as wmill from "../../../gen/services.gen.ts"; +import { formatTimestamp } from "../../utils/utils.ts"; + +function formatBytes(n: number | undefined): string { + if (n == null) return "-"; + if (n < 1024) return `${n}B`; + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}K`; + if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)}M`; + return `${(n / (1024 * 1024 * 1024)).toFixed(2)}G`; +} + +async function listStorages( + opts: GlobalOptions & { json?: boolean } +) { + if (opts.json) log.setSilent(true); + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + + const names = await wmill.getSecondaryStorageNames({ + workspace: workspace.workspaceId, + includeDefault: true, + }); + + if (opts.json) { + console.log(JSON.stringify(names)); + return; + } + if (names.length === 0) { + log.info("No object storage configured for this workspace."); + return; + } + for (const name of names) { + console.log(name === "_default_" ? `${name} ${colors.dim("(default)")}` : name); + } +} + +async function listFiles( + opts: GlobalOptions & { + json?: boolean; + maxKeys?: number; + marker?: string; + storage?: string; + }, + prefix?: string +) { + if (opts.json) log.setSilent(true); + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + + const result = await wmill.listStoredFiles({ + workspace: workspace.workspaceId, + maxKeys: opts.maxKeys ?? 100, + marker: opts.marker, + prefix, + storage: opts.storage, + }); + + if (opts.json) { + console.log(JSON.stringify(result)); + return; + } + const files = result.windmill_large_files ?? []; + if (files.length === 0) { + log.info("No files found."); + return; + } + new Table() + .header(["Key"]) + .padding(2) + .border(true) + .body(files.map((f) => [f.s3])) + .render(); + if (result.next_marker) { + log.info(`\nMore results available. Use --marker '${result.next_marker}' to paginate.`); + } +} + +async function upload( + opts: GlobalOptions & { + storage?: string; + contentType?: string; + contentDisposition?: string; + }, + localPath: string, + fileKey: string +) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + + const buf = await readFile(localPath); + // Wrap Node Buffer in a Blob for the SDK request body. + const blob = new Blob([buf], { type: opts.contentType ?? "application/octet-stream" }); + + await wmill.fileUpload({ + workspace: workspace.workspaceId, + fileKey, + storage: opts.storage, + contentType: opts.contentType, + contentDisposition: opts.contentDisposition, + requestBody: blob, + }); + log.info(colors.green(`Uploaded ${localPath} -> ${fileKey}`)); +} + +async function download( + opts: GlobalOptions & { storage?: string; stdout?: boolean }, + fileKey: string, + outputPath?: string +) { + if (opts.stdout) log.setSilent(true); + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + + // The generated request layer (cli/gen/core/request.ts:getResponseBody) + // routes by Content-Type: binary types → Blob, text/* → string, JSON → object. + // The generated return type is `Blob | File`, which is wrong for non-binary + // responses, so widen to unknown before normalizing. + const body: unknown = await wmill.fileDownload({ + workspace: workspace.workspaceId, + fileKey, + storage: opts.storage, + }); + let buf: Buffer; + if (typeof body === "string") { + buf = Buffer.from(body, "utf-8"); + } else if (body instanceof Blob) { + buf = Buffer.from(await body.arrayBuffer()); + } else if (body instanceof ArrayBuffer) { + buf = Buffer.from(body); + } else if (body == null) { + buf = Buffer.alloc(0); + } else { + buf = Buffer.from(JSON.stringify(body), "utf-8"); + } + + if (opts.stdout) { + process.stdout.write(buf); + return; + } + const dest = outputPath ?? basename(fileKey); + await writeFile(dest, buf); + log.info(colors.green(`Downloaded ${fileKey} -> ${dest}`)); +} + +async function del( + opts: GlobalOptions & { storage?: string; yes?: boolean }, + fileKey: string +) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + + if (!opts.yes) { + const confirmed = await Confirm.prompt({ + message: `Delete '${fileKey}' from object storage${opts.storage ? ` (storage: ${opts.storage})` : ""}?`, + default: false, + }); + if (!confirmed) { + log.info("Aborted."); + return; + } + } + + await wmill.deleteS3File({ + workspace: workspace.workspaceId, + fileKey, + storage: opts.storage, + }); + log.info(colors.green(`Deleted ${fileKey}`)); +} + +async function move( + opts: GlobalOptions & { storage?: string }, + srcFileKey: string, + destFileKey: string +) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + + await wmill.moveS3File({ + workspace: workspace.workspaceId, + srcFileKey, + destFileKey, + storage: opts.storage, + }); + log.info(colors.green(`Moved ${srcFileKey} -> ${destFileKey}`)); +} + +async function info( + opts: GlobalOptions & { json?: boolean; storage?: string }, + fileKey: string +) { + if (opts.json) log.setSilent(true); + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + + const metadata = await wmill.loadFileMetadata({ + workspace: workspace.workspaceId, + fileKey, + storage: opts.storage, + }); + + if (opts.json) { + console.log(JSON.stringify(metadata)); + return; + } + console.log(colors.bold("Key:") + " " + fileKey); + console.log(colors.bold("Size:") + " " + formatBytes(metadata.size_in_bytes)); + console.log(colors.bold("Mime:") + " " + (metadata.mime_type ?? "-")); + console.log( + colors.bold("Last Modified:") + " " + + (metadata.last_modified ? formatTimestamp(metadata.last_modified) : "-") + ); + if (metadata.expires) { + console.log(colors.bold("Expires:") + " " + formatTimestamp(metadata.expires)); + } + if (metadata.version_id) { + console.log(colors.bold("Version Id:") + " " + metadata.version_id); + } +} + +async function preview( + opts: GlobalOptions & { + storage?: string; + bytesFrom?: number; + bytesLength?: number; + csvSeparator?: string; + csvHeader?: boolean; + mime?: string; + }, + fileKey: string +) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + + // Backend requires both byte fields; mirror the frontend's defaults + // (frontend/src/lib/components/S3FilePickerInner.svelte) for an interactive + // peek so the user gets useful output without passing flags. + const result = await wmill.loadFilePreview({ + workspace: workspace.workspaceId, + fileKey, + storage: opts.storage, + fileMimeType: opts.mime, + readBytesFrom: opts.bytesFrom ?? 0, + readBytesLength: opts.bytesLength ?? 128 * 1024, + csvSeparator: opts.csvSeparator, + csvHasHeader: opts.csvHeader, + }); + + if (result.msg) { + log.info(colors.yellow(result.msg)); + } + if (result.content != null) { + process.stdout.write(result.content); + if (!result.content.endsWith("\n")) process.stdout.write("\n"); + } +} + +const command = new Command() + .alias("s3") + .description("Object storage (S3) related commands. Operates on the workspace's default object storage; use --storage to target a configured secondary storage.") + .action(listStorages as any) + .command( + "list", + "List configured object storages for the workspace (default + secondary)." + ) + .option("--json", "Output as JSON (for piping to jq)") + .action(listStorages as any) + .command( + "files", + "List files in an object storage. Optionally filter by prefix." + ) + .alias("ls") + .arguments("[prefix:string]") + .option("--json", "Output as JSON (for piping to jq)") + .option("--max-keys ", "Page size (default 100)") + .option("--marker ", "Pagination marker from a previous response") + .option("--storage ", "Secondary storage name (omit for the workspace default)") + .action(listFiles as any) + .command( + "upload", + "Upload a local file to object storage at the given file key." + ) + .arguments(" ") + .option("--storage ", "Secondary storage name") + .option("--content-type ", "Content-Type header to set on the object") + .option("--content-disposition ", "Content-Disposition header to set on the object") + .action(upload as any) + .command( + "download", + "Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory." + ) + .arguments(" [output_path:string]") + .option("--storage ", "Secondary storage name") + .option("--stdout", "Write file contents to stdout instead of a file") + .action(download as any) + .command( + "delete", + "Delete an object from object storage. Prompts for confirmation unless --yes is set." + ) + .arguments("") + .option("--storage ", "Secondary storage name") + .option("--yes", "Skip the confirmation prompt") + .action(del as any) + .command( + "move", + "Move an object within the same storage (rename or relocate by key)." + ) + .arguments(" ") + .option("--storage ", "Secondary storage name") + .action(move as any) + .command( + "info", + "Show metadata (size, mime, last-modified) for an object." + ) + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .option("--storage ", "Secondary storage name") + .action(info as any) + .command( + "preview", + "Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files." + ) + .arguments("") + .option("--storage ", "Secondary storage name") + .option("--mime ", "Override the detected mime type (e.g. text/csv)") + .option("--bytes-from ", "Start offset in bytes") + .option("--bytes-length ", "Number of bytes to read") + .option("--csv-separator ", "CSV column separator (default ,)") + .option("--csv-header", "Treat the first CSV row as a header") + .action(preview as any); + +export default command; diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index 65ccdf7c2a..714fdec222 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -5167,7 +5167,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se After writing, tell the user which command fits what they want to do: -- \`wmill flow preview \` — **default when iterating on a local flow.** Runs the local \`flow.yaml\` against local inline scripts without deploying. Add \`--remote\` to use deployed workspace scripts for PathScript steps instead of local files. +- \`wmill flow preview \` — **default when iterating on a local flow.** Runs the local \`flow.yaml\` against local inline scripts without deploying. Add \`--remote\` to use deployed workspace scripts for PathScript steps instead of local files. Add \`--step \` to run only one module in isolation (see "Single-step vs whole-flow preview" below). - \`wmill flow run \` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits. - \`wmill generate-metadata\` — regenerate stale \`.lock\` and \`.script.yaml\` files. By default it scans **scripts, flows, and apps** across the workspace; pass \`--skip-flows --skip-apps\` (or run from a subdirectory) to limit the scope when you only care about the flow you edited. - \`wmill sync push\` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test". @@ -5184,6 +5184,12 @@ Only use \`sync push\` when: - The user explicitly asks to deploy, publish, push, or ship. - The preview has already validated the change and the user wants it in the workspace. +### Single-step vs whole-flow preview + +Use \`flow preview --step \` when the user is iterating on one module and the flow's upstream steps aren't part of what they're trying to validate. It runs only that step's runnable (rawscript: the inline script; script: the PathScript, locally if available; flow: the subflow by path) and is much faster than running the whole flow when previous steps are slow or expensive. The step id is resolved by walking nested branchone/branchall/forloopflow/whileloopflow modules and includes the special \`preprocessor\` and \`failure\` modules. + +Use \`flow preview \` (no \`--step\`) when steps depend on each other's outputs, when the user is validating the overall control flow, or when \`--step\` doesn't apply (branchone, branchall, forloopflow, whileloopflow, identity, and AI agent steps cannot themselves be tested in isolation — for branchone/branchall/forloopflow/whileloopflow, the *contained* steps can, by passing the inner step's id). + ### After writing — offer to run, don't wait passively This is about **programmatic execution** (\`wmill flow preview -d ''\`), which actually runs the flow and has side effects. Visual preview (the \`preview\` skill) is offered separately — see "Visual preview" below. @@ -6838,10 +6844,11 @@ flow related commands - \`flow run \` - run a flow by path. - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. - \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting. -- \`flow preview \` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. +- \`flow preview \` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow). - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. - \`-s --silent\` - Do not output anything other then the final output. Useful for scripting. - \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files. + - \`--step \` - Run only the named step instead of the whole flow. Honors --data as the step's args and --remote / local-PathScript resolution the same way the full-flow preview does. - \`flow new \` - create a new empty flow - \`--summary \` - flow summary - \`--description \` - flow description @@ -7057,6 +7064,42 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory - \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks - \`-w, --watch\` - Watch for file changes and re-lint automatically +### object-storage + +**Alias:** \`s3\` + +**Subcommands:** + +- \`object-storage list\` - List configured object storages for the workspace (default + secondary). + - \`--json\` - Output as JSON (for piping to jq) +- \`object-storage files [prefix:string]\` - List files in an object storage. Optionally filter by prefix. + - \`--json\` - Output as JSON (for piping to jq) + - \`--max-keys \` - Page size (default 100) + - \`--marker \` - Pagination marker from a previous response + - \`--storage \` - Secondary storage name (omit for the workspace default) +- \`object-storage upload \` - Upload a local file to object storage at the given file key. + - \`--storage \` - Secondary storage name + - \`--content-type \` - Content-Type header to set on the object + - \`--content-disposition \` - Content-Disposition header to set on the object +- \`object-storage download [output_path:string]\` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory. + - \`--storage \` - Secondary storage name + - \`--stdout\` - Write file contents to stdout instead of a file +- \`object-storage delete \` - Delete an object from object storage. Prompts for confirmation unless --yes is set. + - \`--storage \` - Secondary storage name + - \`--yes\` - Skip the confirmation prompt +- \`object-storage move \` - Move an object within the same storage (rename or relocate by key). + - \`--storage \` - Secondary storage name +- \`object-storage info \` - Show metadata (size, mime, last-modified) for an object. + - \`--json\` - Output as JSON (for piping to jq) + - \`--storage \` - Secondary storage name +- \`object-storage preview \` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files. + - \`--storage \` - Secondary storage name + - \`--mime \` - Override the detected mime type (e.g. text/csv) + - \`--bytes-from \` - Start offset in bytes + - \`--bytes-length \` - Number of bytes to read + - \`--csv-separator \` - CSV column separator (default ,) + - \`--csv-header\` - Treat the first CSV row as a header + ### protection-rules **Subcommands:** @@ -7393,6 +7436,26 @@ workspace related commands - \`--team-name \` - Slack team name - \`workspace disconnect-slack\` + + +# Object Storage CLI + +\`wmill object-storage\` (alias \`wmill s3\`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace \`/job_helpers/*\` endpoints. + +## Key concepts (not obvious from per-command --help) + +- **\`file_key\` is the path inside the bucket** (e.g. \`reports/2026-05/orders.csv\`), not a Windmill path. Do NOT pass \`u/...\` or \`f/...\` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket. +- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target. +- **\`--storage \` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use \`wmill object-storage list\` to discover configured storages. +- **\`preview\` vs \`download\`**: \`preview\` returns a peek (CSV first rows, text content, or a byte slice via \`--bytes-from\`/\`--bytes-length\`) without writing to disk. Use \`download\` when you want the full file on disk. + +## Choosing a subcommand + +- Look at what's there: \`wmill object-storage files [prefix]\` (alias \`ls\`) — paginated, use \`--marker\` to continue. +- Inspect one file: \`wmill object-storage info \` for size/mime/last-modified, \`wmill object-storage preview \` for content peek. +- Move data in: \`wmill object-storage upload \` — set \`--content-type\` if the receiver cares (e.g. \`text/csv\`). +- Move data out: \`wmill object-storage download [output_path]\` — \`--stdout\` to pipe. +- Reorganize: \`wmill object-storage move \` (same storage), \`wmill object-storage delete \` (interactive confirm unless \`--yes\`). `, "preview": `--- name: preview diff --git a/cli/src/main.ts b/cli/src/main.ts index 9faf4e5308..cc8f9e80b7 100755 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -52,6 +52,7 @@ import docs from "./commands/docs/docs.ts"; import config from "./commands/config/config.ts"; import datatable from "./commands/datatable/datatable.ts"; import ducklake from "./commands/ducklake/ducklake.ts"; +import objectStorage from "./commands/object-storage/object-storage.ts"; import { fetchVersion } from "./core/context.ts"; export { @@ -77,6 +78,7 @@ export { config, datatable, ducklake, + objectStorage, hubPull, pull, push, @@ -87,7 +89,7 @@ export { token, }; -export const VERSION = "1.709.0"; +export const VERSION = "1.711.0"; // Re-exported from constants.ts to maintain backwards compatibility export { WM_FORK_PREFIX } from "./core/constants.ts"; @@ -210,6 +212,7 @@ const command = new Command() .command("config", config) .command("datatable", datatable) .command("ducklake", ducklake) + .command("object-storage", objectStorage) .command("version --version", "Show version information") .action(async (opts: any) => { console.log("CLI version: " + VERSION); diff --git a/docs/app-mode-ai-chat-review.md b/docs/app-mode-ai-chat-review.md index 51833d4f9a..eacfba08cb 100644 --- a/docs/app-mode-ai-chat-review.md +++ b/docs/app-mode-ai-chat-review.md @@ -1,354 +1,48 @@ # App Mode AI Chat Review -## Purpose +This note only tracks the highest-value next steps for making app-mode AI chat +safer and more efficient. -This document reviews the current app-mode AI chat design with a focus on: +## Recommended Next Steps -- keeping prompts and context as small as possible; -- requiring user confirmation for important actions; -- making datatable integration smooth and safe for users. +1. Add confirmation for dangerous app tools. -## Short verdict + Require explicit user confirmation before file writes, file deletes, backend + runnable writes, backend runnable deletes, and datatable SQL execution. Show a + useful diff or exact SQL before applying the action. -The app-mode AI chat has a solid foundation: mode-specific helpers, explicit `@` context, app snapshots/revert, datatable whitelisting, and generic confirmation UI already exist. +2. Enforce datatable SQL safety in code. -However, it is not yet optimal for minimal context and user-safe automation: + Do not rely on prompt instructions for SQL safety. Classify statements before + execution, block DDL unless table creation is allowed, and require + confirmation for DDL, DML, and row-returning reads that would expose data back + to the model. -1. **Context is still too large by default**, especially the app system prompt, broad file-discovery guidance, full datatable schemas, and persistent `@` context. (`get_files()` has since been replaced by metadata-only `list_files()`.) -2. **Important app/datatable actions are not consistently confirmed**. The confirmation infrastructure exists, but app tools mostly bypass it. -3. **Datatables UX is promising but has rough edges**: stale cached table context, weak SQL safety, policy persistence issues, and too-heavy full-schema fetching. +3. Keep default context demand-driven. -## Relevant files + Prefer selected context and targeted reads before broad discovery. Keep file + listings metadata-only, avoid sending full datatable schemas by default, and + keep SDK/reference material out of the base prompt unless it is requested or + needed for the task. -### AI chat orchestration +4. Improve app context lifecycle. -- `frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts` -- `frontend/src/lib/components/copilot/chat/chatLoop.ts` -- `frontend/src/lib/components/copilot/chat/shared.ts` -- `frontend/src/lib/components/copilot/chat/AIChat.svelte` -- `frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte` -- `frontend/src/lib/components/copilot/chat/AIChatInput.svelte` -- `frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte` + Treat `@` context as per-message by default, with an explicit pinning affordance + for context that should persist. Lazy-load file and runnable contents, and add + a visible approximate context-size indicator so users can spot prompt bloat. -### App mode +5. Refresh datatable context after mutations. -- `frontend/src/lib/components/copilot/chat/app/core.ts` -- `frontend/src/lib/components/copilot/chat/AppAvailableContextList.svelte` -- `frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte` -- `frontend/src/lib/components/copilot/chat/DatatableCreationPolicy.svelte` + Refresh table metadata after data-panel changes and after AI-created tables so + follow-up tool calls and user-visible context do not use stale schema data. -### Raw app editor and datatables +6. Persist table creation policy explicitly. -- `frontend/src/lib/components/raw_apps/RawAppEditor.svelte` -- `frontend/src/lib/components/raw_apps/RawAppDataTableList.svelte` -- `frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte` -- `frontend/src/lib/components/raw_apps/DefaultDatabaseSelector.svelte` -- `frontend/src/lib/components/raw_apps/dataTableRefUtils.ts` -- `frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts` -- `frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte` -- `frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte` + Store whether AI table creation is enabled as an explicit app setting instead + of inferring it from the presence of datatable configuration. -### Backend datatable APIs +7. Add focused eval coverage for these behaviors. -- `backend/windmill-api-workspaces/src/workspaces.rs` - - `list_datatables` - - `list_datatable_schemas` - - `get_datatable_schema` - - `edit_datatable_config` - -### System prompts - -- `system_prompts/README.md` -- `system_prompts/auto-generated/index.ts` -- `system_prompts/auto-generated/sdks/datatable-typescript.md` -- `system_prompts/auto-generated/sdks/datatable-python.md` - -## How app mode works today - -In the raw app editor, `RawAppEditor.svelte` initializes app-mode AI chat on mount: - -- calls `aiChatManager.saveAndClear()`; -- calls `aiChatManager.changeMode(AIMode.APP)`; -- registers app helpers through `aiChatManager.setAppHelpers(...)`. - -Those app helpers expose operations for: - -- frontend files; -- backend runnables; -- current selected editor context; -- linting; -- app snapshots and revert; -- datatable schema loading; -- SQL execution; -- app table whitelisting. - -When app mode is active, `AIChatManager.changeMode(AIMode.APP)` sets: - -- system prompt: `prepareAppSystemMessage(...)`; -- tools: `getAppTools()`; -- helpers: `appAiChatHelpers`. - -When the user sends a message, `prepareAppUserMessage(...)` builds the user prompt from: - -- current frontend/backend file selection, unless excluded; -- inspector-selected DOM element; -- editor code selection; -- additional `@`-mentioned context; -- the user instructions. - -`runChatLoop(...)` then sends the system message, history, user message, and tool definitions to the selected model. Tool calls go through `processToolCall(...)`, which supports confirmation only when a tool opts into `requiresConfirmation`. - -## Current app tools - -### Read and discovery tools - -These are generally safe without confirmation: - -- `list_files` -- `get_frontend_file` -- `get_backend_runnable` -- `get_selected_context` -- `lint` -- `search_workspace` -- `get_runnable_details` -- `search_hub_scripts` -- `list_datatables` -- `get_datatable_table_schema` - -### Mutating tools - -These currently execute directly in app mode: - -- `set_frontend_file` -- `patch_file` -- `delete_frontend_file` -- `set_backend_runnable` -- `delete_backend_runnable` -- `exec_datatable_sql` - -This is the biggest mismatch with the requirement that every important action should be confirmed by the user. - -## System prompt assessment - -The app system prompt is useful but heavier than ideal. - -### Strengths - -- Clearly explains raw app structure. -- Explains the frontend/backend runnable split. -- Encourages `patch_file` for small edits. -- Pushes datatables for persisted app storage. -- Explains that datatable DDL should go through `exec_datatable_sql`. -- Includes table creation policy context. - -### Concerns - -1. It always includes broad app-building instructions, even for small localized edits. -2. The previous prompt included the datatable SDK reference for both TypeScript and Python every time. This has since been removed; concise examples remain in the prompt. -3. The previous prompt told the model to start with `get_files()`, which encouraged loading all files even when selected context was sufficient. This is now improved by `list_files()`, but the prompt still needs to stay demand-driven. -4. It relies heavily on prompt instructions for datatable safety instead of enforcing safety in tools. -5. Custom workspace/user prompts are appended as `USER GIVEN INSTRUCTIONS`, which is flexible but can further increase context. - -### Recommendation - -The base app prompt should be shorter and more demand-driven: - -- Keep file discovery demand-driven: use selected and explicitly provided context first; call `list_files()` only when a broader metadata overview is needed. -- Keep full SDK details out of the default prompt; concise examples are usually enough. Add an on-demand SDK reference only if it does not cause unnecessary extra tool turns. -- Keep only minimal datatable rules in the base prompt: - - use datatables for persistence; - - call `list_datatables()` before schema work; - - DDL must use `exec_datatable_sql`; - - non-read SQL requires confirmation. - -## Additional context assessment - -The `@` context system is a good UX foundation. - -App mode exposes categories for: - -- frontend files; -- backend runnables; -- datatables. - -Selecting a datatable context includes its columns and also calls `addTableToWhitelist(...)`, adding the table to the app data panel. - -### Strengths - -- Context is explicit and user-controllable. -- Datatable table selection is naturally integrated into the chat input. -- Selected app file/runnable chips are visible and can be excluded. -- Inspector and code-selection context are compact and useful. - -### Concerns - -1. `@` context persists across messages until manually removed, which can silently bloat follow-up prompts. -2. Available app context currently includes file contents/runnable configs in memory before selection. -3. Each selected context item is truncated, but there is no overall context budget indicator. -4. Current file/runnable selection is included by default unless excluded, which is convenient but not minimal. - -### Recommendation - -- Make app `@` context per-message by default. -- Add an explicit “pin” option for context that should persist across messages. -- Lazy-load file/runnable content when selected or when a message is sent. -- Show an approximate context-size/token budget indicator. -- Prefer sending path/name and selected code first; fetch full files only when necessary. - -## Confirmation assessment - -The generic confirmation mechanism already exists: - -- `processToolCall(...)` checks `tool.requiresConfirmation`. -- `ToolExecutionDisplay.svelte` renders Run/Cancel controls. -- Script test runs, flow test runs, and mutating API calls already use confirmation. - -App mode should use the same infrastructure for important actions. - -### Suggested confirmation policy - -#### No confirmation required - -- `list_files`, as a metadata-only response; -- `get_frontend_file`; -- `get_backend_runnable`; -- `get_selected_context`; -- `list_datatables`, as table-name metadata only; -- `get_datatable_table_schema`, as a targeted schema read; -- `lint`; -- search tools. - -#### Confirmation required - -- `set_frontend_file`; -- `patch_file`; -- `delete_frontend_file`; -- `set_backend_runnable`; -- `delete_backend_runnable`; -- `exec_datatable_sql` for any DDL or DML; -- `exec_datatable_sql` for `SELECT` if it returns real row data that will be sent back to the model. - -### Recommended UX - -For files/runnables: - -- Prefer batched proposed edits. -- Show a diff. -- Let the user click “Apply changes”. -- Run lint after applying. - -For SQL: - -- Show the exact SQL. -- Classify the query as: - - schema read; - - data read; - - insert/update/delete; - - DDL. -- Require confirmation before data reads and all mutations. -- For table creation, require both: - - table creation policy enabled; - - explicit confirmation of the `CREATE TABLE` SQL. - -## Datatables integration assessment - -The datatable integration is directionally good and already has several strong user-facing pieces. - -### Current strengths - -The new app setup lets the user choose: - -- default datatable; -- schema mode: none, new, existing; -- whether AI can create tables; -- pre-whitelisted existing tables. - -The raw app data panel lets users: - -- add datatable table references; -- inspect tables through the DB manager drawer; -- configure the default datatable/schema for new tables. - -The AI chat integration lets users: - -- mention datatable tables through `@` context; -- add mentioned tables to the app whitelist; -- list datatable/schema/table names with `list_datatables()`; -- retrieve one table's columns with `get_datatable_table_schema()`; -- create tables through `exec_datatable_sql(..., new_table)`. - -### Concerns - -1. **`exec_datatable_sql` is too powerful without confirmation.** - It can run `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, `ALTER`, etc. - -2. **Table creation policy is not fully enforced in code.** - The tool blocks `new_table` when policy is disabled, but it does not block DDL if the model omits `new_table`. - -3. **Table creation disabled state may not persist cleanly.** - `RawAppData` stores `datatable` and `schema`, but not an explicit `enabled` value. `RawAppEditor` infers enabled from `data.datatable !== undefined`, which can re-enable table creation after reopening. - -4. **Datatable context cache can become stale.** - `AIChatManager.refreshDatatables()` runs when app helpers are set, but may not refresh immediately after data panel changes or after AI creates a new table. - -5. **Full schema loading can still be too expensive internally.** - `list_datatables()` and `get_datatable_table_schema()` reduce what is sent to the model, but they still currently rely on app helpers that fetch full schema data before filtering. - -6. **Auto-whitelisting from `@table` is convenient but silent.** - It mutates app data without an obvious confirmation or undo affordance. - -### Recommended datatable tool design - -Instead of one broad schema tool and one unrestricted SQL tool, prefer smaller tools: - -- `list_datatables()` -- `list_datatable_tables(datatable, schema?, search?)` (optional backend/API optimization if table lists need server-side filtering) -- `get_datatable_table_schema(datatable, schema, table)` -- `preview_datatable_rows(datatable, schema, table, limit)` with confirmation -- `execute_datatable_sql(datatable, sql)` with query classification and confirmation -- `create_datatable_table(datatable, schema, table, columns)` as a structured safe path for table creation - -## Priority recommendations - -1. **Add confirmation to dangerous app tools** - - file/runnable writes; - - file/runnable deletes; - - datatable SQL; - - especially DDL/DML. - -2. **Enforce SQL safety in code, not only in prompts** - - block DDL unless `new_table` is provided and policy allows it; - - confirm all non-`SELECT` statements; - - consider confirming `SELECT` row reads too. - -3. **Reduce default prompt/tool context** - - keep `list_files()` metadata-only and demand-driven; - - use selected context first; - - keep full SDK references out of the default prompt; - - keep datatable tools split into smaller schema/table lookups. - -4. **Refresh datatable context reliably** - - refresh after data panel changes; - - refresh after `exec_datatable_sql(..., new_table)`; - - remove debug logging from datatable refresh. - -5. **Persist table creation policy explicitly** - - store a boolean such as `tableCreationEnabled` in raw app data; - - do not infer enabled solely from `data.datatable`. - -6. **Improve `@` context lifecycle** - - make app `@` context per-message by default; - - add pinning for persistent context; - - lazy-load file/runnable contents; - - show approximate context size. - -## Overall opinion - -The current architecture is good and extensible, but it should become more demand-driven and safer before being considered efficient and user-safe. - -The highest-impact changes are: - -- add confirmation for app mutations and datatable SQL; -- enforce datatable SQL policy programmatically; -- reduce the app system prompt and avoid automatic broad context loading; -- split datatable schema access into smaller, targeted tools. + Cover confirmation requirements, datatable SQL policy enforcement, selected + context minimization, and stale-schema refresh behavior with targeted app-mode + evals or lower-level tests where practical. diff --git a/docs/app-mode-ai-chat-token-baseline.md b/docs/app-mode-ai-chat-token-baseline.md deleted file mode 100644 index 95eab70c0d..0000000000 --- a/docs/app-mode-ai-chat-token-baseline.md +++ /dev/null @@ -1,245 +0,0 @@ -# App Mode AI Chat Token Baseline - -This baseline was collected before optimizing app-mode context/prompt/datatable behavior. - -> Note: The historical commands/results below include `app-token-selected-large-frontend-context` and `app-token-selected-large-backend-context`. Those cases were removed from the active eval suite because `runtime.appContext.selected` only verified that the file/runnable existed and did not serialize a selected file/runnable hint to the model. Future selected-file/runnable coverage should be reintroduced through the app context manager path. - -## Command - -Secrets were loaded from `~/windmill/ai_evals/.env` without printing them. - -```bash -cd ai_evals -set -a -source ~/windmill/ai_evals/.env -set +a -bun run cli -- run app \ - app-token-baseline-large-app-small-edit \ - app-token-selected-large-frontend-context \ - app-token-selected-large-backend-context \ - app-token-many-datatable-context \ - app-token-large-datatable-discovery \ - --model haiku \ - --runs 1 \ - --output results/app-token-baseline-current-max8.json -``` - -## Environment - -- Mode: `app` -- Model under test: `anthropic:claude-haiku-4-5-20251001` -- Transport: `direct` -- Judge model: `claude-sonnet-4-6` -- Runs per case: `1` -- Token-heavy app cases use `runtime.maxTurns: 8` - -## Results - -Pass rate: **100% (5/5)** - -| Case | Prompt tokens | Completion tokens | Total tokens | Tool calls | Tools used | -|---|---:|---:|---:|---:|---| -| `app-token-baseline-large-app-small-edit` | 73,682 | 519 | 74,201 | 4 | `get_files`, `get_frontend_file`, `patch_file` | -| `app-token-selected-large-frontend-context` | 36,305 | 348 | 36,653 | 2 | `get_frontend_file`, `patch_file` | -| `app-token-selected-large-backend-context` | 95,232 | 19,633 | 114,865 | 4 | `set_backend_runnable`, `get_backend_runnable` | -| `app-token-many-datatable-context` | 35,204 | 404 | 35,608 | 2 | `get_files`, `patch_file` | -| `app-token-large-datatable-discovery` | 114,964 | 4,047 | 119,011 | 7 | `get_files`, `get_datatables`, `set_backend_runnable`, `set_frontend_file`, `patch_file`, `lint` | - -Aggregate token usage: - -```json -{ - "totalTokenUsage": { - "prompt": 355387, - "completion": 24951, - "total": 380338 - }, - "averageTokenUsagePerAttempt": { - "prompt": 71077.4, - "completion": 4990.2, - "total": 76067.6 - } -} -``` - -## Interpretation - -The highest-token cases are: - -1. `app-token-large-datatable-discovery` — full datatable discovery with `get_datatables()` and app edits reached **119,011** total tokens. -2. `app-token-selected-large-backend-context` — selected large backend runnable plus a rewrite-style tool call reached **114,865** total tokens. -3. `app-token-baseline-large-app-small-edit` — a trivial heading edit still reached **74,201** total tokens, largely due broad file discovery. - -These cases should be rerun after prompt/context/tool changes to compare total and prompt-token reductions. - -## Follow-up: metadata-only `list_files` - -The contentful `get_files` app-mode tool was replaced with `list_files` to make broad app discovery cheaper and less sticky in chat history. - -Changes: - -- Renamed the overview tool from `get_files` to `list_files`. -- Changed the overview response from truncated source/config contents to metadata only. -- `list_files` returns: - - frontend files: `path`, character `size`, and file `kind`; - - backend runnables: `key`, `name`, `type`, and lightweight optional metadata such as `path`, `language`, `contentSize`, and `staticInputKeys`. -- Updated app-mode prompt guidance so the model no longer starts every task with broad file discovery. -- Kept targeted content tools as the path for inspection: - - `get_frontend_file(path)` for frontend source; - - `get_backend_runnable(key)` for runnable configuration/source. - -The same five cases were rerun with: - -```bash -cd ai_evals -set -a -source ~/windmill/ai_evals/.env -set +a -bun run cli -- run app \ - app-token-baseline-large-app-small-edit \ - app-token-selected-large-frontend-context \ - app-token-selected-large-backend-context \ - app-token-many-datatable-context \ - app-token-large-datatable-discovery \ - --model haiku \ - --runs 1 \ - --output results/app-token-after-list-files.json -``` - -Pass rate: **100% (5/5)** - -| Case | Prompt tokens | Completion tokens | Total tokens | Tool calls | Tools used | -|---|---:|---:|---:|---:|---| -| `app-token-baseline-large-app-small-edit` | 41,020 | 422 | 41,442 | 3 | `list_files`, `get_frontend_file`, `patch_file` | -| `app-token-selected-large-frontend-context` | 41,020 | 422 | 41,442 | 3 | `list_files`, `get_frontend_file`, `patch_file` | -| `app-token-selected-large-backend-context` | 53,511 | 9,714 | 63,225 | 3 | `list_files`, `get_backend_runnable`, `set_backend_runnable` | -| `app-token-many-datatable-context` | 46,990 | 475 | 47,465 | 3 | `list_files`, `get_frontend_file`, `patch_file` | -| `app-token-large-datatable-discovery` | 131,607 | 5,084 | 136,691 | 8 | `get_datatables`, `list_files`, `set_backend_runnable`, `set_frontend_file`, `patch_file`, `lint` | - -Aggregate token usage: - -```json -{ - "totalTokenUsage": { - "prompt": 314148, - "completion": 16117, - "total": 330265 - }, - "averageTokenUsagePerAttempt": { - "prompt": 62829.6, - "completion": 3223.4, - "total": 66053 - } -} -``` - -Comparison against the post-rebase / PR #8922 run (`results/app-token-after-origin-main-pr8922.json`): - -| Case | PR #8922 total | `list_files` total | Delta | Delta % | Prompt delta | -|---|---:|---:|---:|---:|---:| -| `app-token-baseline-large-app-small-edit` | 74,061 | 41,442 | -32,619 | -44.0% | -32,522 | -| `app-token-selected-large-frontend-context` | 74,061 | 41,442 | -32,619 | -44.0% | -32,522 | -| `app-token-selected-large-backend-context` | 71,050 | 63,225 | -7,825 | -11.0% | -7,787 | -| `app-token-many-datatable-context` | 35,497 | 47,465 | +11,968 | +33.7% | +11,886 | -| `app-token-large-datatable-discovery` | 97,128 | 136,691 | +39,563 | +40.7% | +38,295 | - -Aggregate comparison against the post-rebase / PR #8922 run: - -| Metric | PR #8922 | `list_files` | Delta | Delta % | -|---|---:|---:|---:|---:| -| Prompt tokens | 336,798 | 314,148 | -22,650 | -6.7% | -| Completion tokens | 14,999 | 16,117 | +1,118 | +7.5% | -| Total tokens | 351,797 | 330,265 | -21,532 | -6.1% | - -Compared to the original baseline above, the `list_files` run is **-50,073 total tokens** (**-13.2% total**). - -Interpretation: - -- The small edit and selected-frontend cases improved substantially because broad discovery no longer injects truncated contents for the whole app. -- The selected-backend case also improved, despite still needing targeted runnable inspection. -- The datatable-context cases can require an extra `get_frontend_file` after `list_files`, so the small datatable edit regressed in this single-run sample. -- The large datatable case remains dominated by datatable/schema prompt bloat and model variability; moving datatable SDK/reference and schema discovery behind smaller on-demand tools is still the next likely high-impact optimization. - -## Follow-up: targeted datatable tools and shorter datatable prompt - -The next pass reduced default datatable context by making datatable discovery metadata-first and removing the full datatable SDK reference from the system prompt. - -Changes: - -- Replaced the broad schema discovery tool with `list_datatables()` for datatable/schema/table names only. -- Added `get_datatable_table_schema(datatable_name, schema_name, table_name)` for targeted column lookup when column names/types are actually needed. -- Removed the full TypeScript + Python datatable SDK reference from the default app system prompt. -- Kept concise TypeScript and Python datatable examples in the prompt, which were enough for the benchmark cases. -- Strengthened prompt/tool guidance so table-list dashboards use `list_datatables()` directly and avoid schema/SDK lookups unless needed. - -The same five cases were rerun with: - -```bash -cd ai_evals -set -a -source ~/windmill/ai_evals/.env -set +a -bun run cli -- run app \ - app-token-baseline-large-app-small-edit \ - app-token-selected-large-frontend-context \ - app-token-selected-large-backend-context \ - app-token-many-datatable-context \ - app-token-large-datatable-discovery \ - --model haiku \ - --runs 1 \ - --output results/app-token-after-datatable-tools-v3.json -``` - -Pass rate: **100% (5/5)** - -| Case | Prompt tokens | Completion tokens | Total tokens | Tool calls | Tools used | -|---|---:|---:|---:|---:|---| -| `app-token-baseline-large-app-small-edit` | 37,516 | 425 | 37,941 | 3 | `list_files`, `get_frontend_file`, `patch_file` | -| `app-token-selected-large-frontend-context` | 37,516 | 358 | 37,874 | 3 | `list_files`, `get_frontend_file`, `patch_file` | -| `app-token-selected-large-backend-context` | 49,995 | 9,708 | 59,703 | 3 | `list_files`, `get_backend_runnable`, `set_backend_runnable` | -| `app-token-many-datatable-context` | 43,493 | 536 | 44,029 | 3 | `list_files`, `get_frontend_file`, `patch_file` | -| `app-token-large-datatable-discovery` | 24,193 | 2,043 | 26,236 | 4 | `list_datatables`, `list_files`, `get_frontend_file`, `set_frontend_file` | - -Aggregate token usage: - -```json -{ - "totalTokenUsage": { - "prompt": 192713, - "completion": 13070, - "total": 205783 - }, - "averageTokenUsagePerAttempt": { - "prompt": 38542.6, - "completion": 2614, - "total": 41156.6 - } -} -``` - -Comparison against the metadata-only `list_files` run (`results/app-token-after-list-files.json`): - -| Case | `list_files` total | Datatable-tools total | Delta | Delta % | Prompt delta | -|---|---:|---:|---:|---:|---:| -| `app-token-baseline-large-app-small-edit` | 41,442 | 37,941 | -3,501 | -8.4% | -3,504 | -| `app-token-selected-large-frontend-context` | 41,442 | 37,874 | -3,568 | -8.6% | -3,504 | -| `app-token-selected-large-backend-context` | 63,225 | 59,703 | -3,522 | -5.6% | -3,516 | -| `app-token-many-datatable-context` | 47,465 | 44,029 | -3,436 | -7.2% | -3,497 | -| `app-token-large-datatable-discovery` | 136,691 | 26,236 | -110,455 | -80.8% | -107,414 | - -Aggregate comparison: - -| Metric | `list_files` | Datatable tools | Delta | Delta % | -|---|---:|---:|---:|---:| -| Prompt tokens | 314,148 | 192,713 | -121,435 | -38.7% | -| Completion tokens | 16,117 | 13,070 | -3,047 | -18.9% | -| Total tokens | 330,265 | 205,783 | -124,482 | -37.7% | - -Compared to the post-rebase / PR #8922 run, the datatable-tools run is **-146,014 total tokens** (**-41.5% total**). Compared to the original baseline above, it is **-174,555 total tokens** (**-45.9% total**). - -Interpretation: - -- Removing the full datatable SDK reference from the default prompt saved about 3.5k prompt tokens in every case. -- The large datatable discovery case improved dramatically because the model used `list_datatables()` table-name metadata instead of loading full schemas. -- The small datatable-context edit is still higher than the post-rebase / PR #8922 run because selected file identifiers are not yet injected, so the model still discovers and reads `/index.tsx` before patching. -- A future context-manager-backed selected file/runnable flow should add cheap selected identifiers when that UX is ready, so selected-file tasks can skip `list_files()` without reintroducing implicit source-content bloat. diff --git a/docs/failing-tests.md b/docs/failing-tests.md deleted file mode 100644 index d0ae44f109..0000000000 --- a/docs/failing-tests.md +++ /dev/null @@ -1,33 +0,0 @@ -# Failing Tests - -This file tracks benchmark cases that still fail or need follow-up validation. - -## Flow - -- `flow-test6-ai-agent-tools` - Latest failing run: `ai_evals/results/2026-04-09T11-25-24.107Z__flow` - Issues: - final output does not include the actions or tool-result details the prompt asks for - `open_support_ticket` contains a syntax bug - -- `flow-test7-simple-modification` - Latest failing run: `ai_evals/results/2026-04-09T11-25-24.107Z__flow` - Issues: - `validate_data` was added, but the failure behavior still does not match the requested contract - `save_results` throws instead of returning a graceful structured result - -- `flow-test11-preprocessor-and-failure-handler` - Latest failing run: `ai_evals/results/2026-04-09T11-25-24.107Z__flow` - Issues: - the model creates regular `preprocessor` and `failure` modules - it does not use Windmill's special top-level `preprocessor_module` and `failure_module` - -## Needs Reconfirmation - -- `flow-test4-order-processing-loop` - Full-suite failing run: `ai_evals/results/2026-04-09T11-25-24.107Z__flow` - Follow-up passing run after prompt improvement: `ai_evals/results/2026-04-09T13-29-15.877Z__flow` - Note: - this case failed on invalid `branchone` downstream result access - it passed after adding explicit branch-output guidance to the flow prompt - rerun the full flow suite to confirm the fix holds in the broader benchmark diff --git a/docs/system-prompt-testing-plan.md b/docs/system-prompt-testing-plan.md deleted file mode 100644 index 9b12f1c5e0..0000000000 --- a/docs/system-prompt-testing-plan.md +++ /dev/null @@ -1,1000 +0,0 @@ -# System Prompt And Skill Output Testing Plan - -Historical note: - -- This file is a planning document and no longer matches the current benchmark CLI in every detail. -- The current source of truth is [ai_evals/README.md](/home/farhad/windmill__worktrees/prompt-testing-plan/ai_evals/README.md) and [system-prompt-testing-status.md](/home/farhad/windmill__worktrees/prompt-testing-plan/docs/system-prompt-testing-status.md). -- In particular, the current tool no longer has the old variants, compare, or history workflow described below. - -## Goal - -Build a single testing strategy that answers one question reliably: - -> Given a user task, how good is the artifact produced by our AI system? - -This plan is intentionally focused on **black-box output evaluation**, not on unit testing frontend or CLI internals. - -The intended end state is a **new repo-level benchmark CLI** that runs a shared -eval suite across multiple surfaces. - -That benchmark CLI should be the main entrypoint for: - -- running one case -- running a benchmark set -- comparing baseline vs candidate variants -- writing benchmark history snapshots - -Frontend and Windmill CLI are not meant to become separate testing products. -They should be implemented as adapters behind this shared benchmark CLI. - -The system under test is: - -- Frontend AI Chat in `script`, `flow`, and `app` modes -- CLI local development experience driven by generated guidance and skills - -The artifact under test is: - -- Script code -- Flow JSON / module structure -- Raw app files and backend runnables -- Files and project artifacts produced in a local CLI workspace - -## Non-Goals - -This plan does **not** treat the following as the main testing target: - -- Unit testing helper functions, stores, or tool wrapper internals -- UI rendering behavior, DOM interactions, or component-level correctness -- `wmill init` correctness as a standalone product area -- Backend route correctness except where it affects prompt delivery or AI configuration - -Those may still need lightweight tests, but they are not the core of prompt reliability evaluation. - -## Core Principles - -### 1. Black-box evaluation only - -The runner should provide an input task to the real system setup, let it run, collect the final artifact, and score the result. - -In practice, this runner should be exposed through the new repo-level benchmark -CLI rather than through separate ad hoc test commands for each surface. - -### 2. Headless execution - -Frontend evaluation must be fully decoupled from the browser UI. It should exercise prompt assembly, tool selection, and tool execution logic without mounting Svelte components or clicking through the app. - -### 3. Real prompt environment - -All evals must use the same prompt-building path, tool definitions, and skill content that production uses, or a clearly defined variant of them. - -### 4. Artifact-first scoring - -The main score is based on the produced artifact, not on intermediate transcripts. - -### 5. Reliability over one-off success - -A prompt is not "good" because it passed once. Reliability means pass rate across repeated runs and across a representative case set. - -### 6. Track benchmark history over time - -The suite must not only evaluate the current output. It must also produce a -git-tracked benchmark history so the team can see whether the system is -improving over time. - -This history should focus on official benchmark snapshots, not on every local -experiment. - -### 7. Shared corpus, separate adapters - -Frontend and CLI should share the same evaluation corpus format when possible, but each surface should have its own execution adapter. - -### 8. CLI first, UI last - -The CLI should be the first surface brought to a high-confidence benchmark -state. - -It is the cleanest foundation for the suite because it produces direct files in -an isolated workspace, has less ambiguity than the frontend, and is easier to -score deterministically. - -Frontend should reuse the benchmark model proven on the CLI rather than define -a parallel testing philosophy. - -### 9. UI comes last - -The testing suite must exist and be trustworthy before building a studio UI on top of it. - -## Current State - -## Shared Prompt Source Of Truth - -The repo already has the right content split: - -- `system_prompts/` is the shared source of truth for core Windmill prompt content -- frontend adds chat-specific tool instructions on top -- CLI materializes guidance and skill content from generated outputs - -This is a strong foundation for a shared eval suite. - -## Execution Priority - -Even though the repo already has useful frontend eval scaffolding, the -implementation priority should be: - -1. build the repo-level benchmark CLI and use the Windmill CLI adapter as the - first implementation behind it -2. make the CLI artifact-evaluation path excellent -3. stabilize shared scoring, reporting, and benchmark history around that path -4. bring frontend onto the same benchmark model through the same benchmark CLI -5. build the UI only after the underlying suite is trustworthy - -This keeps the hardest product question focused on artifact quality rather than -on UI workflow. - -## Benchmark CLI As The Main Product - -The testing suite should have one primary interface: - -- a new repo-level benchmark CLI - -The benchmark CLI should be able to run: - -- Windmill CLI evals -- frontend evals -- shared reporting and comparison commands - -Illustrative command shape: - -```bash -ai-evals run --surface cli --case bun-hello-script -ai-evals run --surface frontend-flow --case support-flow -ai-evals compare --surface cli --variant baseline --variant candidate-a -ai-evals history latest -``` - -The exact binary name can change, but the architecture should not: - -- one benchmark CLI -- shared case loader -- shared scoring -- shared history writer -- separate surface adapters underneath - -## Temporary Bootstrap Code - -This bootstrap phase is now complete for frontend `flow`, `app`, and `script`. - -Frontend AI benchmark ownership has moved into `ai_evals/`, and the frontend -source tree no longer owns a separate AI benchmark suite under -`frontend/.../__tests__/...`. - -Benchmark authors should only need the repo-level benchmark CLI to run the -long-term suite. - -The only temporary frontend-specific piece that remains is a thin Vitest/Vite -loader bridge so the benchmark runner can import the production chat modules in -the same module/runtime environment they already expect. - -## Frontend: What Exists Today - -The current frontend benchmark path is **decoupled from the UI** and now owned -by `ai_evals`. - -They currently: - -- run through the shared headless chat loop -- use production prompt builders -- use production tool definitions -- use benchmark-owned helper adapters that write to temp workspaces on disk -- execute through the frontend module/runtime environment only as a loader bridge - -This means the current frontend evals are now a proper benchmark adapter, -not a frontend test suite. - -That is the correct direction. - -### Frontend Architecture Notes - -There are three categories of code involved: - -- shared production logic: - - production system prompt builders - - production tool definitions - - production `runChatLoop` -- benchmark-only infrastructure: - - case loading - - variant loading - - judge scoring - - benchmark result shaping - - history/reporting integration -- alternate helper adapters: - - production helpers mutate UI/editor state - - benchmark helpers mutate temp-workspace files - -This is important because the benchmark suite is **not** meant to duplicate the -frontend chat logic. It is meant to reuse the production chat loop and tool -definitions while swapping the execution backend from UI state to filesystem -state. - -## Frontend: What Is Missing - -### Coverage gaps - -- `script` is now exposed through the shared benchmark CLI, but it only has initial case coverage. -- Existing frontend coverage is still too small relative to the target benchmark corpus. - -### Reliability gaps - -- Frontend flow and app can already run with pass/fail results and repeated runs through the shared benchmark CLI. -- The remaining gap is turning that into stronger routine reliability gating with better deterministic validators and broader routine case coverage. -- Frontend reliability reporting is still less mature than the intended end state for official CI tiers and richer failure triage. - -### Prompt-iteration gaps - -- Frontend prompt variants are file-backed now, but the repo only ships baseline manifests by default. -- Creating and curating meaningful frontend candidate variants is still a mostly manual workflow compared with the CLI snapshot flow. -- Frontend prompt comparison exists through the shared `compare` command, but it still needs broader routine use and better variant coverage. - -### Artifact-validation gaps - -- The current flow and app helpers are file-backed now, but several effects are still lightweight and should become more realistic over time. -- Linting and runnable validation are currently too lightweight in the eval path. -- Datatable interactions are mocked rather than validated as output constraints. -- The suite does not yet enforce a strong deterministic validator layer before using an LLM judge. - -### Corpus gaps - -- Frontend surfaces already use shared case manifests under `ai_evals/cases/frontend/`. -- The remaining gap is breadth and representativeness, not the absence of a shared corpus. -- Cases still need richer metadata, stronger deterministic constraints, and a larger regression library built from real failures. - -### Reporting gaps - -- Frontend runs already emit the shared benchmark result shape and can write official history snapshots through the shared benchmark CLI. -- There is still no rich leaderboard or trend-oriented debugging workflow for frontend surfaces specifically. -- There is still no strong "worst failures first" report for debugging regressions. - -## Frontend: Perfect Testing Logic - -The perfect frontend testing logic is: - -Frontend should not be the place where the benchmark philosophy is invented. - -It should consume the shared case format, validator model, reporting format, -and history format already proven through the CLI path. - -### 1. Stay fully headless - -Do not mount the chat UI. - -Do not click through the frontend. - -Do not use Playwright for prompt evaluation. - -The runner should directly invoke: - -- the production system message builder -- the production user message builder -- the production tool list -- the production chat loop - -It is acceptable for the benchmark adapter to use the frontend Vitest/Vite -runtime as a thin loader bridge when production chat modules still depend on -that environment, as long as: - -- the benchmark entrypoint remains the shared benchmark CLI -- the benchmark logic and fixtures live under `ai_evals` -- the frontend source tree does not own a separate benchmark suite - -This keeps the suite decorrelated from the frontend UI while still testing the real AI logic. - -### 2. Test the three frontend AI surfaces separately - -#### Script mode - -Input: - -- user prompt -- optional initial script -- optional context such as selected workspace runnables or DB references - -Output: - -- final script code - -Scoring: - -- deterministic validators first -- LLM judge second - -Deterministic validators should include: - -- expected entrypoint present -- syntax / parse validity -- language-appropriate compile or lint check where feasible -- required behaviors or structures present -- forbidden patterns absent - -#### Flow mode - -Input: - -- user prompt -- optional initial flow -- optional schema -- optional workspace context - -Output: - -- final flow definition - -Scoring: - -- flow JSON is structurally valid -- expected module types exist -- expected branches / loops / tools exist -- schema shape matches required inputs -- required data flow connections are present -- LLM judge scores completeness and overall quality - -#### App mode - -Input: - -- user prompt -- optional initial app -- optional workspace context - -Output: - -- final frontend files -- final backend runnables - -Scoring: - -- expected files and runnables exist -- file structure is coherent -- app bundle / lint checks pass where feasible in headless mode -- required UI/backend behaviors are represented in the artifact -- LLM judge scores completeness and product quality - -### 3. Use repeated runs, not single runs - -Each case should run more than once. - -Recommended starting point: - -- PR smoke run: 2 runs per case on a small curated subset -- nightly reliability run: 5 to 10 runs per case on the full benchmark set - -Primary metric: - -- pass rate - -Secondary metrics: - -- average deterministic score -- average judge score -- worst-case judge score -- latency -- total tool calls - -### 4. Keep tool traces as diagnostics only - -Tool usage matters for debugging, but it should not be the primary score. - -The suite should record: - -- tool names -- tool arguments -- iteration count -- model/provider - -But the main question remains: - -> Was the final artifact good? - -### 5. Make prompt variants easy to test - -Prompt candidates should not require editing test code. - -The suite should support a file-based prompt variant workflow. - -Example direction: - -- `ai_evals/variants/frontend/script/baseline.md` -- `ai_evals/variants/frontend/script/candidate-a.md` -- `ai_evals/variants/frontend/flow/baseline.md` -- `ai_evals/variants/frontend/app/baseline.md` - -Each variant should be runnable side by side against the same case set. - -### 6. Separate benchmark cases from test code - -Benchmark cases should live in data files, not inline in test files. - -Each case should define: - -- surface -- user prompt -- initial artifact if any -- required constraints -- forbidden constraints -- judge rubric -- tags - -This makes the benchmark editable by prompt authors without changing runner logic. - -## CLI: What Exists Today - -The current CLI tests prove only one narrow property: - -> Given a prompt, does the model invoke the expected skill? - -That is useful as a smoke signal, but it is far from sufficient for output evaluation. - -The current CLI setup also depends on manual preparation of a `.claude/skills` folder, which makes repeated benchmarking and prompt iteration much harder than necessary. - -## CLI: What Is Missing - -### Output-evaluation gap - -- The current suite does not score the artifact produced by the CLI workflow. -- It only checks whether a skill was invoked. -- It does not verify that the resulting files are good. - -### Automation gap - -- The current setup requires manual copying of generated skills into a test folder. -- That makes the suite too fragile and too manual for rapid prompt iteration. - -### Reliability gap - -- There is no repeated-run measurement. -- There is no pass-rate metric. -- There is no baseline vs candidate comparison workflow. - -### Prompt-variant gap - -- There is no first-class way to test alternate skill bundles or alternate generated guidance. -- There is no clean candidate flow for "I changed skill content, show me whether reliability improved." - -### Corpus gap - -- CLI cases are not aligned with frontend benchmark cases. -- There is no shared benchmark language describing the task, initial state, and expected artifact. - -### Reporting gap - -- There is no stable output report for artifact comparison. -- There is no failure clustering by skill bundle, task family, or model. - -## CLI: Perfect Testing Logic - -The perfect CLI testing logic is: - -This should be the reference implementation for the suite. - -### 1. Evaluate the final artifact, not the skill invocation - -Skill invocation should be kept as diagnostic metadata only. - -The primary output should be the files produced in a temporary workspace. - -Example CLI artifacts: - -- generated script files -- generated flow files -- raw app project files -- schedule / trigger config files -- AGENTS / guidance files only when they are directly relevant to the task - -### 2. Create the workspace automatically - -The runner should create a fresh temporary project for every case. - -It should seed that workspace with: - -- initial files for the benchmark case -- the current generated CLI guidance and skills -- any fixture data required by the task - -It should never depend on a manually maintained test folder. - -### 3. Materialize the exact skill bundle under test - -The runner should be able to test: - -- the current production skill bundle -- a candidate skill bundle built from prompt changes - -For CLI, a "prompt variant" is effectively a skill-bundle variant. - -That means the suite should support alternate generated skill content without requiring ad hoc manual copies. - -### 4. Score the final workspace - -The scoring approach should match the frontend philosophy: - -- deterministic validators first -- LLM judge second - -Deterministic validators for CLI should include: - -- expected files created -- expected file names and locations -- required content patterns present -- expected artifact type produced -- optional parse / lint / compile validation where feasible - -### 5. Run repeated benchmarks - -The CLI should use the same reliability logic as frontend: - -- benchmark set -- repeated runs -- pass rate -- baseline vs candidate comparison - -### 6. Keep skill traces as diagnostics - -Record: - -- invoked skills -- order of invocation -- turns -- file changes - -But do not let that replace artifact evaluation. - -## Perfect Shared Benchmark Model - -The frontend and CLI should share the same benchmark concept. - -Each evaluation case should define: - -- `id` -- `surface` -- `user_prompt` -- `initial_state` -- `workspace_context` -- `artifact_checks` -- `judge_rubric` -- `tags` - -The same task should be runnable on multiple surfaces when it makes sense. - -This gives direct comparability between: - -- frontend script vs CLI script -- frontend flow vs CLI flow -- frontend app vs CLI app - -## Recommended Benchmark Categories - -The first benchmark set should be broad, but not huge. - -Recommended initial size: - -- 20 to 30 core cases - -Recommended categories: - -- from-scratch script creation -- script modification -- from-scratch flow creation -- flow modification -- from-scratch raw app creation -- raw app modification -- reuse of workspace assets -- tasks requiring datatable awareness -- tasks requiring constraints or edge-case handling -- known regressions from real failures - -Every category should contain both: - -- "easy success" cases -- "high ambiguity" cases - -This is essential for measuring reliability rather than only measuring best-case demos. - -## Scoring Model - -The suite should use three layers. - -## Layer 1: Deterministic Validators - -This is the hard gate. - -Examples: - -- parse succeeds -- artifact shape is valid -- required entrypoint exists -- expected files exist -- required module types exist -- expected inputs / schema fields exist -- forbidden patterns are absent - -If layer 1 fails, the run is a failure. - -## Layer 2: Task-Specific Validators - -These are stronger artifact checks derived from the benchmark case. - -Examples: - -- flow contains a loop and a conditional branch -- app includes a reset button path and backend wiring -- script performs the requested transformation - -These should still be deterministic whenever possible. - -## Layer 3: LLM Judge - -Use an LLM judge only after deterministic validation. - -The judge should answer: - -- Did the artifact satisfy the request? -- Is it complete? -- Is it coherent for Windmill? -- How close is it to the intended solution? - -The judge score is valuable, but it should not be the only oracle. - -## Benchmark History - -The suite should persist official benchmark summaries in a git-tracked history -layer so improvements and regressions can be reviewed over time. - -## What Should Be Git-Tracked - -Only official benchmark outputs should be committed: - -- post-merge benchmark snapshots on `main` -- scheduled nightly benchmark snapshots -- manually promoted benchmark snapshots when the team wants to record a result - -Each official snapshot should produce: - -- one detailed run JSON -- one entry in an append-only summary file -- regenerated rollups for trend views - -## What Should Not Be Git-Tracked - -The following should remain local or external by default: - -- raw transcripts -- full model messages -- large generated artifact bundles -- ad hoc local experiments -- temporary comparison runs - -This keeps git history focused on stable benchmark signals instead of noisy -debug output. - -## Reliability Metrics - -Every prompt or skill candidate should be reported with: - -- total cases -- passes -- pass rate -- average judge score -- median judge score -- worst-case judge score -- average latency -- average turns - -Per-case results should also be retained. - -This is the minimum needed to compare: - -- baseline vs candidate -- provider vs provider -- frontend vs CLI - -## Benchmark Metrics - -The history layer should track metrics in four groups. - -## Quality Metrics - -- `pass_rate` -- `deterministic_pass_rate` -- `judge_score_mean` -- `judge_score_median` -- `judge_score_p10` -- `category_pass_rate` - -## Reliability Metrics - -- `runs_per_case` -- `flake_rate` -- `path_consistency` - -## Efficiency Metrics - -- `latency_ms_mean` -- `latency_ms_median` -- `tokens_prompt_mean` -- `tokens_completion_mean` -- `tokens_total_mean` -- `tool_calls_mean` -- `iterations_mean` -- `estimated_cost_mean` -- `cost_per_success` -- `latency_per_success` - -## Provenance Metrics - -- `timestamp` -- `git_sha` -- `suite_version` -- `scoring_version` -- `surface` -- `variant_name` -- `provider` -- `model` -- `judge_model` - -The provenance metrics are essential. Without them, a trend line can mix prompt -changes with upstream model drift and become hard to interpret. - -## Efficiency Score - -The suite should not collapse everything into one number. - -It should track at least three top-level composite scores: - -- `quality_score` -- `efficiency_score` -- `value_score` - -Recommended interpretation: - -- `quality_score`: how good the artifact is -- `efficiency_score`: how fast and cheap the system is relative to peers -- `value_score`: quality-adjusted efficiency - -These composite scores should sit on top of the raw metrics, not replace them. - -## Proposed Suite Architecture - -The suite should be built in six layers. - -## Layer 1: Benchmark Data - -Purpose: - -- define the cases once - -Contents: - -- case files -- reusable initial fixtures -- evaluation metadata - -## Layer 2: Benchmark CLI - -Purpose: - -- provide one shared entrypoint for the suite - -Responsibilities: - -- load cases and variants -- select a surface adapter -- run one case or a benchmark set -- invoke shared scoring and history writing -- expose comparison and history commands - -## Layer 3: Surface Adapters - -Purpose: - -- run a case against one surface - -Adapters: - -- frontend-script adapter -- frontend-flow adapter -- frontend-app adapter -- CLI adapter - -Responsibilities: - -- prepare the correct prompt environment -- prepare the initial artifact state -- run the real model loop -- return the final artifact plus diagnostics - -## Layer 4: Scoring And Reporting - -Purpose: - -- evaluate the final artifact -- aggregate repeated runs -- compare variants - -Responsibilities: - -- deterministic validation -- LLM judging -- pass/fail computation -- result serialization -- comparison reports - -## Layer 5: Benchmark History - -Purpose: - -- preserve official benchmark summaries over time -- support trend analysis and regression review - -Responsibilities: - -- store official run snapshots -- append benchmark summary entries -- generate rollups for charts and dashboards -- keep provenance metadata for every tracked run - -## Layer 6: UI Studio - -Purpose: - -- provide a user interface for the exact same benchmark CLI and runner stack - -Important rule: - -The UI must not define its own execution semantics. - -It must only be a frontend over the same suite used in CI and local benchmarking. - -## Proposed Development Order - -### Phase 1: Stabilize the benchmark model - -Deliverables: - -- shared case schema -- shared result schema -- initial core benchmark set - -### Phase 2: Build the benchmark CLI shell - -Deliverables: - -- repo-level benchmark CLI entrypoint -- `run`, `compare`, and `history` command skeletons -- adapter selection layer -- temporary wiring to the first CLI adapter - -### Phase 3: Replace the CLI smoke suite with real artifact evaluation - -Deliverables: - -- temp-workspace runner -- automatic skill-bundle materialization -- artifact scoring -- repeated-run support -- baseline vs candidate skill-bundle comparison - -### Phase 4: Add shared reporting and benchmark history around the CLI path - -Deliverables: - -- baseline vs candidate reports -- pass-rate summaries -- worst-failure reports -- official run schema -- git-tracked benchmark summary file -- history snapshot writer -- rollup generation for trend charts - -### Phase 5: Finish the frontend black-box harness on top of the shared model - -Deliverables: - -- convert current flow and app evals into proper scored reliability tests -- add script eval support -- add repeated-run support -- add prompt-variant loading from files -- align frontend outputs with the shared result and history format -- expose frontend runs through the same benchmark CLI - -### Phase 6: Add CI tiers - -Deliverables: - -- fast PR smoke benchmark -- fuller nightly benchmark -- official history updates on `main` and scheduled runs -- manual benchmark mode for prompt authors - -### Phase 7: Build the UI studio - -Deliverables: - -- run selector -- variant selector -- per-case comparison view -- artifact diff view -- reliability dashboard -- trend dashboard backed by git-tracked benchmark history - -This phase comes last because the UI is only valuable once the underlying suite is stable and trusted. - -## Proposed Prompt Variant Workflow - -The suite should make it cheap to test new prompt candidates. - -Recommended workflow: - -1. Edit or add a candidate prompt file. -2. Run the benchmark against baseline and candidate. -3. Compare pass rate and score. -4. Inspect worst regressions first. -5. Promote only if the candidate improves the benchmark materially. - -For CLI, the same workflow applies, but the tested unit is the generated skill bundle rather than a single chat system prompt. - -## Suggested Repository Direction - -This plan does not require the UI studio to exist first. - -A reasonable repo structure would be: - -```text -ai_evals/ - cli/ - cases/ - fixtures/ - history/ - runs/ - rollups/ - variants/ - frontend/ - script/ - flow/ - app/ - cli/ - results/ # gitignored - scripts/ - adapters/ - scoring/ - reports/ -``` - -The exact folder names can change, but the architectural split should remain. - -## What "Done" Looks Like - -This project is successful when all of the following are true: - -- one repo-level benchmark CLI is the primary way to run prompt evals -- frontend prompt behavior is tested headlessly and independently from the UI -- CLI local-dev behavior is tested by evaluating the final files it produces -- benchmark cases are shared where possible between frontend and CLI -- prompt and skill candidates can be tested without editing test code -- reliability is reported as pass rate over repeated runs -- baseline vs candidate comparisons are easy to run and inspect -- the UI studio is only a thin interface over the same trusted runner - -## Final Recommendation - -The current frontend evals should be treated as a useful starting point, not the finished solution. - -They already prove that the repo can test AI behavior without coupling to the browser UI. - -The main work now is: - -- build the repo-level benchmark CLI as the durable entrypoint -- replace CLI invocation checks with artifact evaluation -- make the CLI path the reference benchmark implementation -- unify frontend under that same benchmark model -- make frontend evals complete and reliability-oriented only after the shared - scoring model is stable -- build the UI only after the suite is strong enough to stand on its own diff --git a/docs/system-prompt-testing-status.md b/docs/system-prompt-testing-status.md deleted file mode 100644 index 86beaadc89..0000000000 --- a/docs/system-prompt-testing-status.md +++ /dev/null @@ -1,140 +0,0 @@ -# System Prompt Testing Status - -This document describes the benchmark tool that exists today. It is the current -truth for `ai_evals/`. - -The longer planning document in -[system-prompt-testing-plan.md](/home/farhad/windmill__worktrees/prompt-testing-plan/docs/system-prompt-testing-plan.md) -still contains useful background, but parts of its workflow are now historical -because the old variants/history system was removed. - -## Current Tool - -There is one repo-level benchmark CLI under `ai_evals/` with three commands: - -- `bun run cli -- models` -- `bun run cli -- cases [mode]` -- `bun run cli -- run [caseIds...]` - -Supported modes: - -- `cli` -- `flow` -- `script` -- `app` - -Public `run` options: - -- `--runs ` -- `--output ` -- `--model ` -- `--verbose` -- `--record` - -There is no variant workflow and no compare command in the current tool. -Tracked history is intentionally minimal: `run --record` appends one compact -summary line to `ai_evals/history/.jsonl`. This is only allowed for -full-suite runs, not selected case ids. History lines include average token -usage when the benchmark mode reports it, plus average judge score and per-case -duration/judge/token usage summaries. - -## How It Works - -Each attempt runs: - -1. the current production prompts, tools, and guidance from this checkout -2. deterministic validation -3. LLM judging - -Results are written locally under `ai_evals/results/` as: - -- a summary JSON file -- a sibling artifacts directory containing the generated flow/script/app/workspace - -If `--record` is used, the CLI also appends a compact JSONL summary line to the -tracked file for that mode under `ai_evals/history/`. - -## Current Architecture - -- `ai_evals/cases/`: one YAML manifest per mode -- `ai_evals/fixtures/`: initial and expected fixtures -- `ai_evals/core/`: shared case loading, model resolution, validation, judging, and result writing -- `ai_evals/history/`: optional tracked pass-rate history written by `run --record`, one JSONL file per mode -- `ai_evals/modes/`: one runner per mode - -Execution model: - -- `flow`, `script`, and `app` reuse the production frontend chat loop and production tool definitions through the frontend Vitest bridge -- `cli` creates a temp workspace, writes the current checkout guidance into it, and runs the Anthropic agent SDK against that workspace - -## Case Model - -Each case is intentionally small: - -- `prompt` -- optional `initial` -- optional `expected` -- optional `validate` -- optional `cliExpect` - -`validate` is mainly used for stronger deterministic checks where exact fixture -matching would be too strict, especially for `flow` creation cases. - -`cliExpect` is used by CLI-mode cases to assert agent behavior deterministically, -including: - -- required or forbidden skills -- skills invoked before the first file mutation -- ordered `wmill` command proposals in the assistant response -- forbidden attempted `wmill` executions -- read-only guidance cases where the workspace must stay unchanged - -Examples of current deterministic checks: - -- schema contains one of several accepted input shapes -- `results.*` references resolve -- required code/input characteristics exist in some module -- expected workspace files are created in `cli` mode -- expected CLI skills and proposed `wmill` commands are observed in `cli` mode - -## Model Selection - -Model aliases are resolved through a shared registry in `ai_evals/core/models.ts`. - -Current aliases: - -- `haiku` -- `sonnet` -- `opus` -- `4o` - -Notes: - -- the `models` command also shows accepted alias spellings such as `gpt-4o` and `claude-opus-4.6` -- frontend modes can use Anthropic and OpenAI-backed aliases -- `cli` mode is Anthropic-only because it runs through the Anthropic agent SDK -- the judge model is separate and currently defaults to `claude-sonnet-4-6` - -## What Is Working Well - -- one simple local benchmark CLI -- real production execution paths instead of synthetic prompt variants -- local result and artifact persistence by default -- live frontend progress output -- reusable flow/script/app/cli runners under one tool -- deterministic validation can now catch real runtime-invalid flow wiring - -## What Still Needs Work - -- broader case coverage across all four modes -- stronger deterministic validators for more cases, especially app/script semantics -- clearer per-case validation metadata as the corpus grows -- CI automation for smoke and nightly runs - -## Recommended Next Focus - -The next high-value work is: - -1. add more realistic benchmark cases -2. keep simplifying deterministic validators so they check correctness, not one exact implementation -3. add CI only after the local benchmark signal is trustworthy diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 55499a2b7e..67be947bde 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@windmill-labs/components", - "version": "1.709.0", + "version": "1.711.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@windmill-labs/components", - "version": "1.709.0", + "version": "1.711.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -846,6 +846,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -857,6 +858,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -867,6 +869,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1356,6 +1359,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1504,6 +1508,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1520,6 +1525,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1536,6 +1542,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1552,6 +1559,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1568,6 +1576,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1584,6 +1593,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1600,6 +1610,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1616,6 +1627,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1632,6 +1644,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1648,6 +1661,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1664,6 +1678,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1680,6 +1695,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1696,6 +1712,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1714,6 +1731,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1730,6 +1748,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2035,6 +2054,7 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6810,7 +6830,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7309,6 +7329,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7329,6 +7350,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7349,6 +7371,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7369,6 +7392,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7389,6 +7413,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7409,6 +7434,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7429,6 +7455,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7449,6 +7476,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7469,6 +7497,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7489,6 +7518,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7509,6 +7539,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12077,6 +12108,21 @@ } } }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", @@ -12807,7 +12853,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/frontend/package.json b/frontend/package.json index 723f4075e9..c3651f7daf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@windmill-labs/components", - "version": "1.709.0", + "version": "1.711.0", "scripts": { "dev": "vite dev", "dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev", diff --git a/frontend/src/lib/components/Path.svelte b/frontend/src/lib/components/Path.svelte index b4bd67632e..0a64225d14 100644 --- a/frontend/src/lib/components/Path.svelte +++ b/frontend/src/lib/components/Path.svelte @@ -428,7 +428,7 @@ inputBaseClass, inputBorderClass({ error: !!error }), inputSizeClasses[size], - 'relative flex gap-0 pb-0 mb-1 flex-col flex-wrap sm:flex-row sm:items-center', + 'relative flex gap-0 pb-0 mb-1 flex-wrap flex-row items-center', disabled && '!bg-surface-disabled cursor-not-allowed border-none' )} > @@ -486,7 +486,7 @@ /> {:else if meta.ownerKind === 'folder'} -