From 1493aa7634ff35d0994f879297e323fbed57bec7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:03:03 +0000 Subject: [PATCH 01/13] chore(deps): bump serde_with and time out of vulnerable ranges - serde_with 3.1.0 -> 3.23.0: fixes GHSA-7gcf-g7xr-8hxj (KeyValueMap serialization panic on empty inner entries). serde_with >=3.21 requires serde_core ^1.0.225, so the serde/serde_json pins move to =1.0.229/=1.0.151 to satisfy it. - time 0.3.44 -> 0.3.55: fixes CVE-2026-25727 (RFC 2822 parse stack exhaustion). Reached transitively via cookie/plist/tauri-codegen/ tauri-plugin-updater. Both are transitive-only deps; no app code calls the affected surfaces directly. glib GHSA-wrw7-89jp-8q8g intentionally untouched: 0.20 needs the whole gtk-rs 0.20 ecosystem via tauri, which is an upstream pin. Co-Authored-By: Epinephrine --- desktop/src-tauri/Cargo.lock | 313 +++++++++++++++++++++++++---------- desktop/src-tauri/Cargo.toml | 4 +- 2 files changed, 230 insertions(+), 87 deletions(-) diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 95bc1a674a6..1ca69abe851 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -363,6 +363,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -704,9 +713,9 @@ checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" [[package]] name = "darling" -version = "0.20.11" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ "darling_core", "darling_macro", @@ -714,27 +723,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -748,14 +756,44 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1703,7 +1741,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -1867,6 +1905,8 @@ checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -1932,6 +1972,60 @@ dependencies = [ "system-deps", ] +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -2270,9 +2364,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -2787,6 +2881,21 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -3024,6 +3133,26 @@ dependencies = [ "thiserror 2.0.20", ] +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "regex" version = "1.13.1" @@ -3247,6 +3376,30 @@ dependencies = [ "uuid", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -3295,10 +3448,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ + "serde_core", "serde_derive", ] @@ -3313,15 +3467,24 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -3337,14 +3500,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -3390,15 +3554,20 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.1.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" dependencies = [ - "base64 0.21.7", + "base64 0.23.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "serde", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -3406,14 +3575,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.1.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -3757,7 +3926,7 @@ dependencies = [ "unicode-segmentation", "url", "windows", - "windows-core 0.61.2", + "windows-core", "windows-version", "x11-dl", ] @@ -3854,7 +4023,7 @@ dependencies = [ "glob", "heck 0.5.0", "json-patch", - "schemars", + "schemars 0.8.22", "semver", "serde", "serde_json", @@ -3913,7 +4082,7 @@ dependencies = [ "anyhow", "glob", "plist", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "tauri-utils", @@ -3945,7 +4114,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "open", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "tauri", @@ -3977,7 +4146,7 @@ dependencies = [ "open", "os_pipe", "regex", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "shared_child", @@ -4108,7 +4277,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "schemars", + "schemars 0.8.22", "semver", "serde", "serde-untagged", @@ -4198,30 +4367,29 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -4847,7 +5015,7 @@ dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows", - "windows-core 0.61.2", + "windows-core", "windows-implement", "windows-interface", ] @@ -4871,7 +5039,7 @@ checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.20", "windows", - "windows-core 0.61.2", + "windows-core", ] [[package]] @@ -4927,7 +5095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -4939,7 +5107,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.2", + "windows-core", ] [[package]] @@ -4951,21 +5119,8 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-result", + "windows-strings", ] [[package]] @@ -4974,7 +5129,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link 0.1.3", "windows-threading", ] @@ -5019,7 +5174,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link 0.1.3", ] @@ -5032,15 +5187,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -5050,15 +5196,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -5405,7 +5542,7 @@ dependencies = [ "webkit2gtk-sys", "webview2-com", "windows", - "windows-core 0.61.2", + "windows-core", "windows-version", "x11-dl", ] @@ -5612,6 +5749,12 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + [[package]] name = "zvariant" version = "5.15.0" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 2548b000cd0..d9629b09391 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -16,8 +16,8 @@ tauri-build = { version = "=2.6.3", features = [] } [dependencies] reqwest = { version = "=0.12.24", default-features = false, features = ["json", "rustls-tls"] } -serde = { version = "=1.0.219", features = ["derive"] } -serde_json = "=1.0.140" +serde = { version = "=1.0.229", features = ["derive"] } +serde_json = "=1.0.151" uuid = { version = "=1.18.1", features = ["v4"] } tauri = { version = "=2.11.6", features = ["tray-icon", "image-png"] } tauri-plugin-autostart = "=2.5.0" From bc840a8c04e52457a611c756801b7df7d29a14da Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:05:24 +0000 Subject: [PATCH 02/13] chore(deps): raise rust-version to 1.88 serde_with >=3.21 and time >=0.3.47 both declare rust_version 1.88; the manifest floor follows the dependency graph. CI builds on `stable`, well above 1.88. Co-Authored-By: Epinephrine --- desktop/src-tauri/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index d9629b09391..030ae447e4e 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -5,7 +5,7 @@ description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" edition = "2021" -rust-version = "1.77" +rust-version = "1.88" [lib] name = "opencodex_desktop_lib" From 04611a7b6904b86a1f7159e13f09901548cd3a6b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:03:02 +0000 Subject: [PATCH 03/13] ci: retrigger platform-macos (macos 1/2 wedged inside client-connect.test.ts at 13:42:58, 20-min timeout on 6863dc54; environmental runner stall, no branch code involvement) From d1b0afb056911e0802ca5133043943ccee698787 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:54:01 +0000 Subject: [PATCH 04/13] fix(desktop): replace map_or with is_none_or for clippy 1.98 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- desktop/src-tauri/src/companion_usage.rs | 2 +- desktop/src-tauri/src/native_tray_data.rs | 2 +- desktop/src-tauri/src/native_tray_snapshot.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/src-tauri/src/companion_usage.rs b/desktop/src-tauri/src/companion_usage.rs index 2e1f8dc6205..a6fd6e75613 100644 --- a/desktop/src-tauri/src/companion_usage.rs +++ b/desktop/src-tauri/src/companion_usage.rs @@ -19,7 +19,7 @@ pub fn selected(settings: &Value, row: &Value) -> bool { let provider = text(row, "provider"); let model = text(row, "model"); !hidden(settings, provider) - && settings["models"].as_array().map_or(true, |models| { + && settings["models"].as_array().is_none_or(|models| { models.iter().any(|item| { item.as_str() .is_some_and(|item| item == model || item == format!("{provider}/{model}")) diff --git a/desktop/src-tauri/src/native_tray_data.rs b/desktop/src-tauri/src/native_tray_data.rs index e2a3f258009..095d56976fe 100644 --- a/desktop/src-tauri/src/native_tray_data.rs +++ b/desktop/src-tauri/src/native_tray_data.rs @@ -131,7 +131,7 @@ pub async fn load( .filter(|source| { settings .as_ref() - .map_or(true, |s| !snapshot::hidden(s, &source.name)) + .is_none_or(|s| !snapshot::hidden(s, &source.name)) }) .collect(); Some(json!(load_providers(&proxy, selected).await)) diff --git a/desktop/src-tauri/src/native_tray_snapshot.rs b/desktop/src-tauri/src/native_tray_snapshot.rs index c663c781fb7..c81247fa305 100644 --- a/desktop/src-tauri/src/native_tray_snapshot.rs +++ b/desktop/src-tauri/src/native_tray_snapshot.rs @@ -7,7 +7,7 @@ pub fn display_settings(settings: Option<&Value>) -> Value { json!({ "showToday": enabled("showToday"), "show30Days": settings.is_some(), "showChart": enabled("showChart"), "showModels": enabled("showModels"), - "showAccounts": settings.map_or(true, |s| s["showAccounts"].as_bool() != Some(false)), + "showAccounts": settings.is_none_or(|s| s["showAccounts"].as_bool() != Some(false)), "showCost": enabled("showCost"), "chartStyle": settings.map(|s| text(s, "chartStyle")).unwrap_or("line") }) From 7323584bf8aec7ce456b6b1d82f0f523a2b360be Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:58:18 +0000 Subject: [PATCH 05/13] fix(desktop): rewrite map_or defaults as match arms (clippy + MSRV 1.77) Option::is_none_or requires Rust 1.82 but the crate pins rust-version = "1.77"; a match arm preserves the same semantics under -D warnings + clippy::incompatible_msrv. Co-Authored-By: Epinephrine --- desktop/src-tauri/src/companion_usage.rs | 9 +++++---- desktop/src-tauri/src/native_tray_data.rs | 7 +++---- desktop/src-tauri/src/native_tray_snapshot.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/desktop/src-tauri/src/companion_usage.rs b/desktop/src-tauri/src/companion_usage.rs index a6fd6e75613..33e0ec8f145 100644 --- a/desktop/src-tauri/src/companion_usage.rs +++ b/desktop/src-tauri/src/companion_usage.rs @@ -19,12 +19,13 @@ pub fn selected(settings: &Value, row: &Value) -> bool { let provider = text(row, "provider"); let model = text(row, "model"); !hidden(settings, provider) - && settings["models"].as_array().is_none_or(|models| { - models.iter().any(|item| { + && match settings["models"].as_array() { + None => true, + Some(models) => models.iter().any(|item| { item.as_str() .is_some_and(|item| item == model || item == format!("{provider}/{model}")) - }) - }) + }), + } } const TOTAL_KEYS: [&str; 9] = [ diff --git a/desktop/src-tauri/src/native_tray_data.rs b/desktop/src-tauri/src/native_tray_data.rs index 095d56976fe..2e60f7d78a9 100644 --- a/desktop/src-tauri/src/native_tray_data.rs +++ b/desktop/src-tauri/src/native_tray_data.rs @@ -128,10 +128,9 @@ pub async fn load( let value = if let Some(sources) = sources { let selected = sources .into_iter() - .filter(|source| { - settings - .as_ref() - .is_none_or(|s| !snapshot::hidden(s, &source.name)) + .filter(|source| match settings.as_ref() { + None => true, + Some(s) => !snapshot::hidden(s, &source.name), }) .collect(); Some(json!(load_providers(&proxy, selected).await)) diff --git a/desktop/src-tauri/src/native_tray_snapshot.rs b/desktop/src-tauri/src/native_tray_snapshot.rs index c81247fa305..994a4740202 100644 --- a/desktop/src-tauri/src/native_tray_snapshot.rs +++ b/desktop/src-tauri/src/native_tray_snapshot.rs @@ -7,7 +7,7 @@ pub fn display_settings(settings: Option<&Value>) -> Value { json!({ "showToday": enabled("showToday"), "show30Days": settings.is_some(), "showChart": enabled("showChart"), "showModels": enabled("showModels"), - "showAccounts": settings.is_none_or(|s| s["showAccounts"].as_bool() != Some(false)), + "showAccounts": match settings { None => true, Some(s) => s["showAccounts"].as_bool() != Some(false) }, "showCost": enabled("showCost"), "chartStyle": settings.map(|s| text(s, "chartStyle")).unwrap_or("line") }) From 87d09a7574a76354746a2ba403514b5c1b664e69 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:19:41 +0000 Subject: [PATCH 06/13] fix(chat): relay wire keepalives on the direct Chat encoder The direct Chat encoder mapped heartbeat to ensureRole, so once the role chunk was out the stall-watchdog keepalive emitted no bytes at all; the converter path emits the role chunk plus the ': opencodex heartbeat' SSE comment (#5806). Emit the same comment via emitKeepalive through a shared constant so idle-sensitive Chat clients get the same liveness bytes on both paths, and teach the parity test's normalizeFrames to compare comment-only blocks as raw text instead of crashing on empty data. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/chat/outbound.ts | 9 ++++++++- src/protocols/encoders/chat.ts | 7 ++++++- tests/responses/protocol-direct-encoders-chat.test.ts | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/chat/outbound.ts b/src/chat/outbound.ts index 817aa11b7fb..6c5b6d4de49 100644 --- a/src/chat/outbound.ts +++ b/src/chat/outbound.ts @@ -138,6 +138,13 @@ export function dataFrame(payload: Rec | "[DONE]"): string { return `data: ${JSON.stringify(payload)}\n\n`; } +/** + * The Chat wire's keepalive: an SSE comment, so idle-sensitive clients receive bytes without a + * semantic chunk parsers or usage counters could mistake for model output. Shared with the + * direct encoder (src/protocols/encoders/chat.ts) so both paths emit byte-identical keepalives. + */ +export const CHAT_COMPLETIONS_HEARTBEAT_COMMENT = ": opencodex heartbeat\n\n"; + export function chunkBase(id: string, model: string, created: number): Rec { return { id, @@ -618,7 +625,7 @@ export function responsesSseToChatCompletionsSse( // that signal as an SSE comment so idle-sensitive Chat clients receive bytes without // inventing a semantic chunk that parsers, usage counters, or progress watchdogs could // mistake for model output. - enqueueLiveFrame(encoder.encode(": opencodex heartbeat\n\n")); + enqueueLiveFrame(encoder.encode(CHAT_COMPLETIONS_HEARTBEAT_COMMENT)); emittedFrames++; break; case "response.output_text.delta": { diff --git a/src/protocols/encoders/chat.ts b/src/protocols/encoders/chat.ts index 8c09bce2f17..10fa455c076 100644 --- a/src/protocols/encoders/chat.ts +++ b/src/protocols/encoders/chat.ts @@ -12,6 +12,7 @@ import type { AdapterEvent } from "../../types"; import type { TranslatorBudget } from "../../lib/translator-budget"; import { responsesUsage } from "../../bridge/internal"; import { + CHAT_COMPLETIONS_HEARTBEAT_COMMENT, chatCompletionsErrorResponse, chatCompletionsFailedResponse, chatCompletionsIncompleteOutcome, @@ -122,7 +123,11 @@ function createChatCompletionWriter(sink: ClientFrameSink, model: string): Clien return { start: ensureRole, - heartbeat: ensureRole, + heartbeat() { + // The converter emits the role chunk, then the wire keepalive as an SSE comment. + ensureRole(); + sink.emitKeepalive(CHAT_COMPLETIONS_HEARTBEAT_COMMENT); + }, text(delta) { if (!delta) return; ensureRole(); diff --git a/tests/responses/protocol-direct-encoders-chat.test.ts b/tests/responses/protocol-direct-encoders-chat.test.ts index 36c56cb1f5a..df1b545debc 100644 --- a/tests/responses/protocol-direct-encoders-chat.test.ts +++ b/tests/responses/protocol-direct-encoders-chat.test.ts @@ -58,6 +58,8 @@ function normalizeFrames(text: string): unknown[] { return text.split("\n\n").filter(block => block.trim().length > 0).map(block => { const data = block.split("\n").filter(line => line.startsWith("data:")).map(line => line.slice(5).trim()).join(""); if (data === "[DONE]") return "[DONE]"; + // Comment-only blocks (the wire heartbeat) stay comparable as raw text. + if (data === "") return block; const parsed = JSON.parse(data) as Record; if ("id" in parsed) parsed.id = "ID"; if ("created" in parsed) parsed.created = 0; From 9db5450b31b276066b651c864720db14045ffd56 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:30:47 +0000 Subject: [PATCH 07/13] fix(encoders): keep wire keepalives out of the relayed-event counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emitKeepalive now skips the delivery recorder: heartbeat comments are wire bytes, not delivered model events — matching the legacy bridge whose heartbeat bypasses the counter. Without this, every idle heartbeat tick inflated attempt.relayedEvents. Adds a focused test that drains a hanging Chat stream, fires the watchdog's beat ticks, and asserts the emitted keepalives leave onRelayed untouched (mutation-checked: counting them makes the test fail). Co-Authored-By: Epinephrine --- src/protocols/encoders/adapter-events.ts | 8 ++-- .../protocol-direct-encoders-chat.test.ts | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/protocols/encoders/adapter-events.ts b/src/protocols/encoders/adapter-events.ts index b298e4114ae..d6da94a1f59 100644 --- a/src/protocols/encoders/adapter-events.ts +++ b/src/protocols/encoders/adapter-events.ts @@ -186,7 +186,7 @@ export function encodeAdapterEventStream( const relayed = (observation?: RelayedEventObservation) => { try { hooks.onRelayed?.(observation ?? {}); } catch { /* counters never break the stream */ } }; - const enqueueCharged = (text: string, observation?: RelayedEventObservation, activity = true): void => { + const enqueueCharged = (text: string, observation?: RelayedEventObservation, activity = true, countAsRelayed = true): void => { if (closed) return; const frame = textEncoder.encode(text); const reservation = budget.reserveTransient(frame.byteLength, { kind: "live_transient" }); @@ -201,7 +201,9 @@ export function encodeAdapterEventStream( queuedFrameBytes.push(frame.byteLength); emittedFrames++; if (activity) wireActivity = true; - relayed(observation); + // Keepalives are wire bytes, not delivered model events — the legacy bridge's wire-silence + // heartbeat likewise bypasses the relayed-event counter. + if (countAsRelayed) relayed(observation); }; const sink: ClientFrameSink = { budget, @@ -244,7 +246,7 @@ export function encodeAdapterEventStream( closed = true; } }, - emitKeepalive: text => enqueueCharged(text, undefined, false), + emitKeepalive: text => enqueueCharged(text, undefined, false, false), desiredSize: () => controller.desiredSize ?? 0, }; const writer = createWriter(sink); diff --git a/tests/responses/protocol-direct-encoders-chat.test.ts b/tests/responses/protocol-direct-encoders-chat.test.ts index df1b545debc..f194a37269d 100644 --- a/tests/responses/protocol-direct-encoders-chat.test.ts +++ b/tests/responses/protocol-direct-encoders-chat.test.ts @@ -386,4 +386,48 @@ describe("direct Chat encoder stream lifecycle", () => { expect(directFrames).toEqual(legacyFrames); expect(JSON.stringify(directFrames.at(-1))).toContain("upstream_stall_timeout"); }); + + test("an idle Chat heartbeat stays off the relayed-event counter", async () => { + const ticks: (() => void)[] = []; + const timers = { + setInterval: (handler: () => void) => { ticks.push(handler); return ticks.length - 1; }, + clearInterval: () => {}, + }; + async function* hang(): AsyncGenerator { + yield { type: "text_delta", text: "waiting" }; + await new Promise(() => {}); + } + let relayedEvents = 0; + const stream = encodeChatCompletionSse(hang(), { + ...directOptions(), + heartbeatMs: 1_000, + stallTimeoutSec: 60, + timers, + hooks: { onRelayed: () => { relayedEvents++; } }, + }); + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let body = ""; + // Carry one pending read across drains; a timed-out read is not abandoned but resumed next drain. + let pending: Promise> | undefined; + const drain = async () => { + while (true) { + const read = pending ?? reader.read(); + pending = undefined; + const settled = await Promise.race([read, Bun.sleep(10).then(() => undefined)]); + if (settled === undefined) { pending = read; break; } + if (settled.done) return; + body += decoder.decode(settled.value); + } + }; + await drain(); + const baseline = relayedEvents; + // The first tick only clears the just-consumed upstream/wire activity flags; the heartbeats + // the watchdog emits afterwards are the keepalive comments under test. + for (let i = 0; i < 3; i++) for (const tick of ticks) tick(); + await drain(); + expect(body).toContain(": opencodex heartbeat"); + expect(relayedEvents).toBe(baseline); + await reader.cancel(); + }); }); From 4bd231d79af405b38fa0128bcd2ed5a5b8f26295 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2026 09:37:39 +0000 Subject: [PATCH 08/13] =?UTF-8?q?chore:=20reduce=20PR=20to=20the=20pure=20?= =?UTF-8?q?dependency=20bump=20=E2=80=94=20encoder=20and=20MSRV=20fixes=20?= =?UTF-8?q?landed=20separately?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Epinephrine --- desktop/src-tauri/src/companion_usage.rs | 9 ++++----- desktop/src-tauri/src/native_tray_data.rs | 7 ++++--- desktop/src-tauri/src/native_tray_snapshot.rs | 2 +- src/chat/outbound.ts | 9 +-------- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/desktop/src-tauri/src/companion_usage.rs b/desktop/src-tauri/src/companion_usage.rs index 33e0ec8f145..2e1f8dc6205 100644 --- a/desktop/src-tauri/src/companion_usage.rs +++ b/desktop/src-tauri/src/companion_usage.rs @@ -19,13 +19,12 @@ pub fn selected(settings: &Value, row: &Value) -> bool { let provider = text(row, "provider"); let model = text(row, "model"); !hidden(settings, provider) - && match settings["models"].as_array() { - None => true, - Some(models) => models.iter().any(|item| { + && settings["models"].as_array().map_or(true, |models| { + models.iter().any(|item| { item.as_str() .is_some_and(|item| item == model || item == format!("{provider}/{model}")) - }), - } + }) + }) } const TOTAL_KEYS: [&str; 9] = [ diff --git a/desktop/src-tauri/src/native_tray_data.rs b/desktop/src-tauri/src/native_tray_data.rs index 2e60f7d78a9..e2a3f258009 100644 --- a/desktop/src-tauri/src/native_tray_data.rs +++ b/desktop/src-tauri/src/native_tray_data.rs @@ -128,9 +128,10 @@ pub async fn load( let value = if let Some(sources) = sources { let selected = sources .into_iter() - .filter(|source| match settings.as_ref() { - None => true, - Some(s) => !snapshot::hidden(s, &source.name), + .filter(|source| { + settings + .as_ref() + .map_or(true, |s| !snapshot::hidden(s, &source.name)) }) .collect(); Some(json!(load_providers(&proxy, selected).await)) diff --git a/desktop/src-tauri/src/native_tray_snapshot.rs b/desktop/src-tauri/src/native_tray_snapshot.rs index 994a4740202..c663c781fb7 100644 --- a/desktop/src-tauri/src/native_tray_snapshot.rs +++ b/desktop/src-tauri/src/native_tray_snapshot.rs @@ -7,7 +7,7 @@ pub fn display_settings(settings: Option<&Value>) -> Value { json!({ "showToday": enabled("showToday"), "show30Days": settings.is_some(), "showChart": enabled("showChart"), "showModels": enabled("showModels"), - "showAccounts": match settings { None => true, Some(s) => s["showAccounts"].as_bool() != Some(false) }, + "showAccounts": settings.map_or(true, |s| s["showAccounts"].as_bool() != Some(false)), "showCost": enabled("showCost"), "chartStyle": settings.map(|s| text(s, "chartStyle")).unwrap_or("line") }) diff --git a/src/chat/outbound.ts b/src/chat/outbound.ts index 7e6ba8461af..8e1b684861b 100644 --- a/src/chat/outbound.ts +++ b/src/chat/outbound.ts @@ -138,13 +138,6 @@ export function dataFrame(payload: Rec | "[DONE]"): string { return `data: ${JSON.stringify(payload)}\n\n`; } -/** - * The Chat wire's keepalive: an SSE comment, so idle-sensitive clients receive bytes without a - * semantic chunk parsers or usage counters could mistake for model output. Shared with the - * direct encoder (src/protocols/encoders/chat.ts) so both paths emit byte-identical keepalives. - */ -export const CHAT_COMPLETIONS_HEARTBEAT_COMMENT = ": opencodex heartbeat\n\n"; - export function chunkBase(id: string, model: string, created: number): Rec { return { id, @@ -627,7 +620,7 @@ export function responsesSseToChatCompletionsSse( // that signal as an SSE comment so idle-sensitive Chat clients receive bytes without // inventing a semantic chunk that parsers, usage counters, or progress watchdogs could // mistake for model output. - enqueueLiveFrame(encoder.encode(CHAT_COMPLETIONS_HEARTBEAT_COMMENT)); + enqueueLiveFrame(encoder.encode(": opencodex heartbeat\n\n")); emittedFrames++; break; case "response.output_text.delta": { From a1c68155170123658c13ef6712965758b95ef34f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:24:51 +0000 Subject: [PATCH 09/13] fix(desktop): replace map_or with is_none_or for clippy 1.98 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- desktop/src-tauri/src/companion_usage.rs | 2 +- desktop/src-tauri/src/native_tray_data.rs | 2 +- desktop/src-tauri/src/native_tray_snapshot.rs | 2 +- desktop/src-tauri/src/widget.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/src-tauri/src/companion_usage.rs b/desktop/src-tauri/src/companion_usage.rs index 2e1f8dc6205..a6fd6e75613 100644 --- a/desktop/src-tauri/src/companion_usage.rs +++ b/desktop/src-tauri/src/companion_usage.rs @@ -19,7 +19,7 @@ pub fn selected(settings: &Value, row: &Value) -> bool { let provider = text(row, "provider"); let model = text(row, "model"); !hidden(settings, provider) - && settings["models"].as_array().map_or(true, |models| { + && settings["models"].as_array().is_none_or(|models| { models.iter().any(|item| { item.as_str() .is_some_and(|item| item == model || item == format!("{provider}/{model}")) diff --git a/desktop/src-tauri/src/native_tray_data.rs b/desktop/src-tauri/src/native_tray_data.rs index e2a3f258009..095d56976fe 100644 --- a/desktop/src-tauri/src/native_tray_data.rs +++ b/desktop/src-tauri/src/native_tray_data.rs @@ -131,7 +131,7 @@ pub async fn load( .filter(|source| { settings .as_ref() - .map_or(true, |s| !snapshot::hidden(s, &source.name)) + .is_none_or(|s| !snapshot::hidden(s, &source.name)) }) .collect(); Some(json!(load_providers(&proxy, selected).await)) diff --git a/desktop/src-tauri/src/native_tray_snapshot.rs b/desktop/src-tauri/src/native_tray_snapshot.rs index c663c781fb7..c81247fa305 100644 --- a/desktop/src-tauri/src/native_tray_snapshot.rs +++ b/desktop/src-tauri/src/native_tray_snapshot.rs @@ -7,7 +7,7 @@ pub fn display_settings(settings: Option<&Value>) -> Value { json!({ "showToday": enabled("showToday"), "show30Days": settings.is_some(), "showChart": enabled("showChart"), "showModels": enabled("showModels"), - "showAccounts": settings.map_or(true, |s| s["showAccounts"].as_bool() != Some(false)), + "showAccounts": settings.is_none_or(|s| s["showAccounts"].as_bool() != Some(false)), "showCost": enabled("showCost"), "chartStyle": settings.map(|s| text(s, "chartStyle")).unwrap_or("line") }) diff --git a/desktop/src-tauri/src/widget.rs b/desktop/src-tauri/src/widget.rs index 1ea015025b7..40c4e2c44c8 100644 --- a/desktop/src-tauri/src/widget.rs +++ b/desktop/src-tauri/src/widget.rs @@ -296,8 +296,8 @@ mod macos { last_reload: Option, now: f64, ) -> bool { - previous.map_or(true, |previous| displayed(previous) != displayed(snapshot)) - && last_reload.map_or(true, |last| now - last >= RELOAD_INTERVAL_SECONDS) + previous.is_none_or(|previous| displayed(previous) != displayed(snapshot)) + && last_reload.is_none_or(|last| now - last >= RELOAD_INTERVAL_SECONDS) } static LAST_RELOAD: std::sync::Mutex> = std::sync::Mutex::new(None); From 05cc88a1e916ef2581b6a18180c70f5020659bb5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:53:08 +0000 Subject: [PATCH 10/13] fix(ci): scope enforce-target resolver to same-repo PRs A CodeRabbit status on a commit that heads a PR in another repository of the fork network made resolve-pr publish that foreign PR number, and the write job's pulls.get then 404'd (run 36319316747, resolved 6086 which lives in lidge-jun/opencodex). Filter associated-commit candidates by base repo owner/name so only this repository's PRs resolve. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/enforce-pr-target.yml | 16 ++++++++++++++- tests/ci-workflows/ci-workflows.test.ts | 26 ++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/enforce-pr-target.yml b/.github/workflows/enforce-pr-target.yml index cd8ad762e1e..027b0919d17 100644 --- a/.github/workflows/enforce-pr-target.yml +++ b/.github/workflows/enforce-pr-target.yml @@ -81,7 +81,21 @@ jobs: github.rest.repos.listPullRequestsAssociatedWithCommit, { owner, repo, commit_sha: statusSha, per_page: 100 } ); - candidates = associatedPrs.filter( + // The index spans the whole fork network: a commit that heads + // a PR in another repository comes back with that PR's number, + // which `pulls.get` cannot resolve in THIS repository (404). + // Only a candidate based on this repository is usable identity. + const inRepoPrs = associatedPrs.filter( + candidate => + candidate.base?.repo?.owner?.login === owner && + candidate.base?.repo?.name === repo + ); + if (inRepoPrs.length !== associatedPrs.length) { + core.info( + `Ignored ${associatedPrs.length - inRepoPrs.length} associated PR(s) based on other repositories in the fork network.` + ); + } + candidates = inRepoPrs.filter( candidate => candidate.state === "open" && candidate.head?.sha === statusSha diff --git a/tests/ci-workflows/ci-workflows.test.ts b/tests/ci-workflows/ci-workflows.test.ts index 90b946186fb..d5e30d79739 100644 --- a/tests/ci-workflows/ci-workflows.test.ts +++ b/tests/ci-workflows/ci-workflows.test.ts @@ -3268,7 +3268,12 @@ describe("GitHub Actions hardening", () => { eventName: "status", statusSha: headSha, associatedPullRequests: [ - { number: 42, state: "open", head: { sha: headSha } }, + { + number: 42, + state: "open", + head: { sha: headSha }, + base: { repo: { name: "opencodex", owner: { login: "lidge-jun" } } }, + }, ], }); @@ -3277,6 +3282,25 @@ describe("GitHub Actions hardening", () => { expect(callsTo(result, "pulls.list")).toEqual([]); }); + test("the resolver ignores associated PRs based on another fork-network repo", async () => { + const headSha = "5fa700fb7f7247cbc000038652c60297f868517c"; + const result = await runResolver({ + pr: { base: { ref: "dev" }, number: 42, head: { sha: headSha } }, + eventName: "status", + statusSha: headSha, + associatedPullRequests: [ + // The commit-to-PR index spans the fork network: this number belongs + // to a PR in another repository and 404s on pulls.get here. + { number: 6086, state: "open", head: { sha: headSha }, base: { repo: { name: "opencodex", owner: { login: "fork-parent" } } } }, + { number: 42, state: "open", head: { sha: headSha }, base: { repo: { name: "opencodex", owner: { login: "lidge-jun" } } } }, + ], + }); + + expect(result.outputs).toEqual([{ name: "pull-number", value: "42" }]); + expect(result.logs.join(" ")).toContain("other repositories in the fork network"); + expect(callsTo(result, "pulls.list")).toEqual([]); + }); + test("the resolver fails closed when both resolution paths error", async () => { const headSha = "6c42d17f213a632fc2def56053f0cd574b13d459"; const result = await runResolver({ From a68789540de0dc2682fd773d9333844275cfef27 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 13:29:27 +0000 Subject: [PATCH 11/13] fix(desktop): repair mangled merge resolution in companion_usage::selected The merge of origin/dev at b02ffca85 combined the branch's is_none_or version with dev's match tail, leaving an unclosed delimiter that broke cargo fmt --check. Restore the branch-side is_none_or resolution. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- desktop/src-tauri/src/companion_usage.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src-tauri/src/companion_usage.rs b/desktop/src-tauri/src/companion_usage.rs index 5c8ab484214..a6fd6e75613 100644 --- a/desktop/src-tauri/src/companion_usage.rs +++ b/desktop/src-tauri/src/companion_usage.rs @@ -23,8 +23,8 @@ pub fn selected(settings: &Value, row: &Value) -> bool { models.iter().any(|item| { item.as_str() .is_some_and(|item| item == model || item == format!("{provider}/{model}")) - }), - } + }) + }) } const TOTAL_KEYS: [&str; 9] = [ From f55f37999729e90104878278720f7e19323ff60a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 13:30:52 +0000 Subject: [PATCH 12/13] fix(desktop): convert remaining bool map_or sites for rust-version 1.88 With the branch's MSRV bump to 1.88, clippy::unnecessary_map_or fires on Option::map_or calls returning bool literals; convert the two remaining sites to is_none_or so the desktop clippy lane stays clean. Co-Authored-By: Epinephrine --- desktop/src-tauri/src/supervisor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src-tauri/src/supervisor.rs b/desktop/src-tauri/src/supervisor.rs index 430686ee332..ffccc60b52a 100644 --- a/desktop/src-tauri/src/supervisor.rs +++ b/desktop/src-tauri/src/supervisor.rs @@ -355,7 +355,7 @@ fn input(app: &AppHandle, stale_pid: bool, requested_restart: bool, attempts: u3 Input { phase: supervision.map_or(ExitPhase::Draining, |s| s.phase), wanted: supervision.is_some_and(|s| s.wanted), - reason_set: supervision.map_or(true, |s| s.reason_set), + reason_set: supervision.is_none_or(|s| s.reason_set), startup_running: app .try_state::() .is_some_and(|startup| startup.is_running()), @@ -372,7 +372,7 @@ fn on_exit(app: &AppHandle, pid: u32, exit: SidecarExit) { }; let stale = app .try_state::() - .map_or(true, |state| state.child_pid() != Some(pid)); + .is_none_or(|state| state.child_pid() != Some(pid)); let requested = exit.code == Some(REQUESTED_RESTART_EXIT_CODE); let attempts = supervisor.state().attempts; let verdict = decide(input(app, stale, requested, attempts)); From 541a933e9b6111a280ab31894fd94dff4641fd1b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 13:45:24 +0000 Subject: [PATCH 13/13] test(claude): keep freePortPair below the ephemeral range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The helper probes a random port in 20000-50000 and releases it before startClaudeIntercept binds the same port. On Linux that range overlaps the ephemeral range (32768-60999), so the lifecycle's own port-0 TLS listener — or any kernel-assigned bind in between — can take the observed port and the real listen fails EADDRINUSE (seen in CI as test 1/4: 'only the app's browser tunnels on Desktop's egress proxy consult the picker'). Narrow the pick to 20000-31999, below every platform's ephemeral floor. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/claude-integration/claude-picker-runtime.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/claude-integration/claude-picker-runtime.test.ts b/tests/claude-integration/claude-picker-runtime.test.ts index d94a176dfaa..127b06a2a35 100644 --- a/tests/claude-integration/claude-picker-runtime.test.ts +++ b/tests/claude-integration/claude-picker-runtime.test.ts @@ -267,9 +267,12 @@ async function canBind(port: number): Promise { }); } +// Probing releases the port before the intercept binds it, so the pick must stay below the +// ephemeral floor (32768 on Linux, 49152 on macOS/Windows): a port-0 bind inside startup — +// the lifecycle's own TLS listener — is otherwise free to take the observed pair. async function freePortPair(): Promise { for (let attempt = 0; attempt < 50; attempt += 1) { - const port = 20_000 + Math.floor(Math.random() * 30_000); + const port = 20_000 + Math.floor(Math.random() * 12_000); if (await canBind(port) && await canBind(port + 1)) return port; } throw new Error("no free port pair");