diff --git a/shared/rust-bridge/Cargo.lock b/shared/rust-bridge/Cargo.lock index 8ab3f60f6..080955851 100644 --- a/shared/rust-bridge/Cargo.lock +++ b/shared/rust-bridge/Cargo.lock @@ -315,7 +315,7 @@ dependencies = [ [[package]] name = "alleycat-bridge-core" version = "0.1.0" -source = "git+https://github.com/dnakov/alleycat.git?rev=417f2a9fe38cbed63754f0af7df61f32ec3034e6#417f2a9fe38cbed63754f0af7df61f32ec3034e6" +source = "git+https://github.com/DatScreamer/alleycat.git?rev=eead5e5a4664ed8713183afad10ba8c1389b15fc#eead5e5a4664ed8713183afad10ba8c1389b15fc" dependencies = [ "alleycat-codex-proto", "anyhow", @@ -334,7 +334,7 @@ dependencies = [ [[package]] name = "alleycat-claude-bridge" version = "0.1.0" -source = "git+https://github.com/dnakov/alleycat.git?rev=417f2a9fe38cbed63754f0af7df61f32ec3034e6#417f2a9fe38cbed63754f0af7df61f32ec3034e6" +source = "git+https://github.com/DatScreamer/alleycat.git?rev=eead5e5a4664ed8713183afad10ba8c1389b15fc#eead5e5a4664ed8713183afad10ba8c1389b15fc" dependencies = [ "alleycat-bridge-core", "alleycat-codex-proto", @@ -358,7 +358,7 @@ dependencies = [ [[package]] name = "alleycat-codex-proto" version = "0.1.0" -source = "git+https://github.com/dnakov/alleycat.git?rev=417f2a9fe38cbed63754f0af7df61f32ec3034e6#417f2a9fe38cbed63754f0af7df61f32ec3034e6" +source = "git+https://github.com/DatScreamer/alleycat.git?rev=eead5e5a4664ed8713183afad10ba8c1389b15fc#eead5e5a4664ed8713183afad10ba8c1389b15fc" dependencies = [ "chrono", "serde", @@ -369,7 +369,7 @@ dependencies = [ [[package]] name = "alleycat-opencode-bridge" version = "0.1.0" -source = "git+https://github.com/dnakov/alleycat.git?rev=417f2a9fe38cbed63754f0af7df61f32ec3034e6#417f2a9fe38cbed63754f0af7df61f32ec3034e6" +source = "git+https://github.com/DatScreamer/alleycat.git?rev=eead5e5a4664ed8713183afad10ba8c1389b15fc#eead5e5a4664ed8713183afad10ba8c1389b15fc" dependencies = [ "alleycat-bridge-core", "anyhow", @@ -393,7 +393,7 @@ dependencies = [ [[package]] name = "alleycat-pi-bridge" version = "0.1.0" -source = "git+https://github.com/dnakov/alleycat.git?rev=417f2a9fe38cbed63754f0af7df61f32ec3034e6#417f2a9fe38cbed63754f0af7df61f32ec3034e6" +source = "git+https://github.com/DatScreamer/alleycat.git?rev=eead5e5a4664ed8713183afad10ba8c1389b15fc#eead5e5a4664ed8713183afad10ba8c1389b15fc" dependencies = [ "alleycat-bridge-core", "alleycat-codex-proto", @@ -4208,7 +4208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 1.0.109", ] [[package]] @@ -4529,7 +4529,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4909,7 +4909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6955,7 +6955,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -7041,7 +7041,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.58.0", ] [[package]] @@ -7678,7 +7678,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7772,7 +7772,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8764,7 +8764,7 @@ dependencies = [ "pin-project-lite", "rustc-hash 2.1.1", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tokio-stream", @@ -8807,7 +8807,7 @@ checksum = "02bba20e097a5a16cd0ad14ec882fae1e80a092a124e9422fc4dddd92e96a647" dependencies = [ "cfg_aliases 0.2.2", "libc", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", "windows-sys 0.61.2", ] @@ -8845,7 +8845,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -9000,7 +9000,7 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.22.1", + "base64 0.21.7", "chrono", "getrandom 0.2.17", "http 1.4.0", @@ -10241,7 +10241,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -10278,9 +10278,9 @@ dependencies = [ "cfg_aliases 0.2.2", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -11302,7 +11302,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -11361,7 +11361,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -11720,7 +11720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -12979,7 +12979,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -14467,7 +14467,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/shared/rust-bridge/Cargo.toml b/shared/rust-bridge/Cargo.toml index 84bc1eda2..360210072 100644 --- a/shared/rust-bridge/Cargo.toml +++ b/shared/rust-bridge/Cargo.toml @@ -23,11 +23,13 @@ codex-config = { path = "../third_party/codex/codex-rs/config" } codex-utils-absolute-path = { path = "../third_party/codex/codex-rs/utils/absolute-path" } codex-git-utils = { path = "../third_party/codex/codex-rs/git-utils" } # Pinned to the same immutable commit as the kittylitter daemon so the phone -# and host share one immutable Alleycat release revision. -alleycat-bridge-core = { git = "https://github.com/dnakov/alleycat.git", rev = "417f2a9fe38cbed63754f0af7df61f32ec3034e6" } -alleycat-pi-bridge = { git = "https://github.com/dnakov/alleycat.git", rev = "417f2a9fe38cbed63754f0af7df61f32ec3034e6" } -alleycat-claude-bridge = { git = "https://github.com/dnakov/alleycat.git", rev = "417f2a9fe38cbed63754f0af7df61f32ec3034e6" } -alleycat-opencode-bridge = { git = "https://github.com/dnakov/alleycat.git", rev = "417f2a9fe38cbed63754f0af7df61f32ec3034e6" } +# and host share one immutable Alleycat release revision. All crates come from +# the same source so the `Bridge` trait identity is consistent; forked until +# the project-aware opencode session enumeration lands upstream (dnakov/alleycat). +alleycat-bridge-core = { git = "https://github.com/DatScreamer/alleycat.git", rev = "eead5e5a4664ed8713183afad10ba8c1389b15fc" } +alleycat-pi-bridge = { git = "https://github.com/DatScreamer/alleycat.git", rev = "eead5e5a4664ed8713183afad10ba8c1389b15fc" } +alleycat-claude-bridge = { git = "https://github.com/DatScreamer/alleycat.git", rev = "eead5e5a4664ed8713183afad10ba8c1389b15fc" } +alleycat-opencode-bridge = { git = "https://github.com/DatScreamer/alleycat.git", rev = "eead5e5a4664ed8713183afad10ba8c1389b15fc" } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "io-util"] } diff --git a/shared/rust-bridge/codex-mobile-client/src/store/reconcile.rs b/shared/rust-bridge/codex-mobile-client/src/store/reconcile.rs index b024f0296..96c8843bc 100644 --- a/shared/rust-bridge/codex-mobile-client/src/store/reconcile.rs +++ b/shared/rust-bridge/codex-mobile-client/src/store/reconcile.rs @@ -56,7 +56,15 @@ impl MobileClient { wire_method, response, )?; - self.apply_thread_read_response(server_id, response) + let params = downcast_public_rpc_params::( + wire_method, + params.map(|value| value as &dyn Any), + )?; + self.apply_thread_read_response_for_request( + server_id, + response, + params.include_turns, + ) .map(|_| ()) .map_err(RpcError::Deserialization) } @@ -252,9 +260,27 @@ impl MobileClient { server_id: &str, response: &upstream::ThreadReadResponse, ) -> Result { + self.apply_thread_read_response_for_request(server_id, response, true) + } + + fn apply_thread_read_response_for_request( + &self, + server_id: &str, + response: &upstream::ThreadReadResponse, + include_turns: bool, + ) -> Result { + let mut upstream_thread = response.thread.clone(); + if !include_turns { + // Treat the request contract as authoritative. Some compatibility + // bridges have returned a full archive even for metadata-only + // reads; accepting it bypasses bounded thread/turns/list hydration + // and can replace a five-turn page with hundreds of UI items. + upstream_thread.turns.clear(); + } + let upstream_turns = upstream_thread.turns.clone(); let mut snapshot = crate::thread_snapshot_from_upstream_thread_with_overrides( server_id, - response.thread.clone(), + upstream_thread, None, None, response.approval_policy.map(Into::into), @@ -269,15 +295,15 @@ impl MobileClient { // `load_thread_turns_page` stored. A legacy (or authoritative) // response with embedded turns clears the cursor because the // embedded list is the full history. - apply_pagination_merge(existing.as_ref(), &mut snapshot, &response.thread.turns); + apply_pagination_merge(existing.as_ref(), &mut snapshot, &upstream_turns); // thread/read is authoritative for `initial_turns_loaded`: if the // server returned no turns AND no prior state exists, treat the // thread as having no history rather than a pending page load, so // the iOS spinner doesn't stick (task #10 invariant). - if existing.is_none() && response.thread.turns.is_empty() { + if include_turns && existing.is_none() && upstream_turns.is_empty() { snapshot.initial_turns_loaded = true; } - crate::reconcile_active_turn(existing.as_ref(), &mut snapshot, &response.thread.turns); + crate::reconcile_active_turn(existing.as_ref(), &mut snapshot, &upstream_turns); self.app_store.upsert_thread_snapshot(snapshot); Ok(key) } @@ -1499,4 +1525,86 @@ mod tests { "existing paged items must be preserved when embedded turns are empty" ); } + + #[tokio::test] + async fn metadata_only_thread_read_ignores_bridge_embedded_history() { + let client = MobileClient::new(); + client.app_store.upsert_server( + &ServerConfig { + server_id: "srv".to_string(), + display_name: "Server".to_string(), + host: "localhost".to_string(), + port: 8390, + websocket_url: None, + is_local: true, + tls: false, + }, + ServerHealthSnapshot::Connected, + ); + + let info = crate::types::ThreadInfo { + id: "thread-1".to_string(), + title: None, + model: None, + status: crate::types::ThreadSummaryStatus::Idle, + preview: None, + cwd: None, + path: None, + model_provider: None, + agent_nickname: None, + agent_role: None, + parent_thread_id: None, + forked_from_id: None, + agent_status: None, + created_at: None, + updated_at: None, + }; + let mut primed = ThreadSnapshot::from_info("srv", info); + primed.items = vec![item_with_turn("paged-turn", "paged-item")]; + primed.older_turns_cursor = Some("older-cursor".to_string()); + primed.initial_turns_loaded = true; + client.app_store.upsert_thread_snapshot(primed); + + let mut violating_thread = test_upstream_thread("thread-1"); + violating_thread.turns = vec![upstream::Turn { + id: "unbounded-turn".to_string(), + status: upstream::TurnStatus::Completed, + items: vec![upstream::ThreadItem::UserMessage { + id: "unbounded-item".to_string(), + content: vec![upstream::UserInput::Text { + text: "bridge returned history despite includeTurns=false".to_string(), + text_elements: Vec::new(), + }], + }], + items_view: upstream::TurnItemsView::Full, + error: None, + started_at: None, + completed_at: None, + duration_ms: None, + }]; + let response = upstream::ThreadReadResponse { + thread: violating_thread, + approval_policy: None, + sandbox: None, + }; + let params = upstream::ThreadReadParams { + thread_id: "thread-1".to_string(), + include_turns: false, + }; + + client + .reconcile_public_rpc("thread/read", "srv", Some(¶ms), &response) + .await + .expect("metadata-only thread/read reconciliation"); + + let key = ThreadKey { + server_id: "srv".to_string(), + thread_id: "thread-1".to_string(), + }; + let snapshot = client.app_store.thread_snapshot(&key).expect("snapshot"); + assert_eq!(snapshot.items.len(), 1); + assert_eq!(snapshot.items[0].id, "paged-item"); + assert_eq!(snapshot.older_turns_cursor.as_deref(), Some("older-cursor")); + assert!(snapshot.initial_turns_loaded); + } } diff --git a/tools/scripts/update-alleycat-main.sh b/tools/scripts/update-alleycat-main.sh index 97d5d4eda..cb9cc8ca1 100755 --- a/tools/scripts/update-alleycat-main.sh +++ b/tools/scripts/update-alleycat-main.sh @@ -45,7 +45,7 @@ resolve_alleycat_main() { } alleycat_is_pinned() { - grep -q 'dnakov/alleycat\.git.*rev = ' "$1" + grep -q '\.git.*rev = ' "$1" } update_shared() {