diff --git a/.trajectories/completed/2026-05/traj_9gq96irkj00s.json b/.trajectories/completed/2026-05/traj_9gq96irkj00s.json new file mode 100644 index 000000000..86051d25f --- /dev/null +++ b/.trajectories/completed/2026-05/traj_9gq96irkj00s.json @@ -0,0 +1,53 @@ +{ + "id": "traj_9gq96irkj00s", + "version": 1, + "task": { + "title": "Update relay to use published relaycast Rust reclaim fix" + }, + "status": "completed", + "startedAt": "2026-05-10T18:45:02.118Z", + "completedAt": "2026-05-10T18:48:11.532Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-10T18:46:06.108Z" + } + ], + "chapters": [ + { + "id": "chap_4ntnm8594eai", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-10T18:46:06.108Z", + "endedAt": "2026-05-10T18:48:11.532Z", + "events": [ + { + "ts": 1778438766109, + "type": "decision", + "content": "Use relaycast 1.0.1 register_or_get_agent for strict-name reclaim: Use relaycast 1.0.1 register_or_get_agent for strict-name reclaim", + "raw": { + "question": "Use relaycast 1.0.1 register_or_get_agent for strict-name reclaim", + "chosen": "Use relaycast 1.0.1 register_or_get_agent for strict-name reclaim", + "alternatives": [], + "reasoning": "The Rust SDK now tolerates the public agent payload and owns the get-agent plus rotate-token flow, so relay no longer needs direct HttpClient JSON parsing or urlencoding." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Bumped relay to relaycast 1.0.1 and routed strict-name agent startup through the SDK-owned register_or_get_agent reclaim path, removing the relay-local raw JSON/urlencoding workaround.", + "approach": "Standard approach", + "confidence": 0.93 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay-relaycast-sdk-update", + "tags": [], + "_trace": { + "startRef": "bffd6b21275090f2648701d2005e875f2ca882c6", + "endRef": "bffd6b21275090f2648701d2005e875f2ca882c6" + } +} diff --git a/.trajectories/completed/2026-05/traj_9gq96irkj00s.md b/.trajectories/completed/2026-05/traj_9gq96irkj00s.md new file mode 100644 index 000000000..a1c66b9ef --- /dev/null +++ b/.trajectories/completed/2026-05/traj_9gq96irkj00s.md @@ -0,0 +1,33 @@ +# Trajectory: Update relay to use published relaycast Rust reclaim fix + +> **Status:** ✅ Completed +> **Confidence:** 93% +> **Started:** May 10, 2026 at 08:45 PM +> **Completed:** May 10, 2026 at 08:48 PM + +--- + +## Summary + +Bumped relay to relaycast 1.0.1 and routed strict-name agent startup through the SDK-owned register_or_get_agent reclaim path, removing the relay-local raw JSON/urlencoding workaround. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Use relaycast 1.0.1 register_or_get_agent for strict-name reclaim + +- **Chose:** Use relaycast 1.0.1 register_or_get_agent for strict-name reclaim +- **Reasoning:** The Rust SDK now tolerates the public agent payload and owns the get-agent plus rotate-token flow, so relay no longer needs direct HttpClient JSON parsing or urlencoding. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Use relaycast 1.0.1 register_or_get_agent for strict-name reclaim: Use relaycast 1.0.1 register_or_get_agent for strict-name reclaim diff --git a/.trajectories/index.json b/.trajectories/index.json index a577d252d..c3ec50376 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -2,6 +2,13 @@ "version": 1, "lastUpdated": "2026-05-10T15:29:41.965Z", "trajectories": { + "traj_9gq96irkj00s": { + "title": "Update relay to use published relaycast Rust reclaim fix", + "status": "completed", + "startedAt": "2026-05-10T18:45:02.118Z", + "completedAt": "2026-05-10T18:48:11.532Z", + "path": ".trajectories/completed/2026-05/traj_9gq96irkj00s.json" + }, "traj_1775914133873_35667beb": { "title": "fix-sdk-build-resolution-workflow", "status": "completed", diff --git a/Cargo.lock b/Cargo.lock index a3bf3231b..61f1d2e8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ version = 4 name = "agent-relay-broker" version = "3.0.0" dependencies = [ + "alacritty_terminal", "anyhow", "axum", "chrono", @@ -32,6 +33,7 @@ dependencies = [ "tokio", "tower", "tracing", + "tracing-appender", "tracing-subscriber", "urlencoding", "uuid", @@ -46,6 +48,31 @@ dependencies = [ "memchr", ] +[[package]] +name = "alacritty_terminal" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59" +dependencies = [ + "base64 0.22.1", + "bitflags 2.10.0", + "home", + "libc", + "log", + "miow", + "parking_lot", + "piper", + "polling", + "regex-automata", + "rustix 1.1.3", + "rustix-openpty", + "serde", + "signal-hook 0.4.4", + "unicode-width", + "vte", + "windows-sys 0.59.0", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -111,6 +138,12 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ascii-canvas" version = "3.0.0" @@ -427,6 +460,9 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] [[package]] name = "block-buffer" @@ -584,6 +620,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -602,7 +647,7 @@ dependencies = [ "mio", "parking_lot", "rustix 0.38.44", - "signal-hook", + "signal-hook 0.3.18", "signal-hook-mio", "winapi", ] @@ -632,12 +677,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "data-encoding" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -951,6 +1011,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "hostname" version = "0.4.2" @@ -1492,6 +1561,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "miow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -1533,6 +1611,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-traits" version = "0.2.19" @@ -1693,6 +1777,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1908,9 +1998,9 @@ checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "relaycast" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e7eb6ecfa6b2b3599f4367c50e511575111a69ebe61556b472ad107802a32aa" +checksum = "d887af9016823e94bf5efd35c4855f3ff96b415e595fc62833c66229567e8170" dependencies = [ "futures-util", "reqwest", @@ -2010,6 +2100,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustix-openpty" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de16c7c59892b870a6336f185dc10943517f1327447096bbb7bb32cd85e2393" +dependencies = [ + "errno", + "libc", + "rustix 1.1.3", +] + [[package]] name = "rustls" version = "0.23.36" @@ -2297,6 +2398,16 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-mio" version = "0.2.5" @@ -2305,7 +2416,7 @@ checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", - "signal-hook", + "signal-hook 0.3.18", ] [[package]] @@ -2392,6 +2503,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -2516,6 +2633,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -2674,6 +2822,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -2779,6 +2940,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2857,6 +3024,20 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" +dependencies = [ + "arrayvec", + "bitflags 2.10.0", + "cursor-icon", + "log", + "memchr", + "serde", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -3078,6 +3259,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" diff --git a/Cargo.toml b/Cargo.toml index e006c3d64..0683a79ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,15 +29,17 @@ serde_json = "1.0" sha2 = "0.10" shlex = "1.3" thiserror = "2.0" -relaycast = "=1.0.0" +relaycast = "=1.0.2" tokio = { version = "1.44", features = ["full"] } tracing = "0.1" +tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } tempfile = "3.19" crossterm = { version = "0.28", features = ["event-stream"] } futures-lite = "2.6" uuid = { version = "1.15", features = ["v4", "serde"] } urlencoding = "2.1" +alacritty_terminal = "0.26" [target.'cfg(unix)'.dependencies] nix = { version = "0.30", features = ["signal", "process", "term", "fs"] } diff --git a/src/auth.rs b/src/auth.rs index cd7684192..dfebfbbfe 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,8 +1,6 @@ use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; -use relaycast::{ - ClientOptions, CreateAgentRequest, HttpClient, RelayCast, RelayCastOptions, RelayError, -}; +use relaycast::{CreateAgentRequest, RelayCast, RelayCastOptions, RelayError}; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -609,6 +607,25 @@ impl AuthClient { .map(ToOwned::to_owned) .unwrap_or_else(|| format!("agent-{}", Uuid::new_v4().simple())); + if strict_name { + let request = CreateAgentRequest { + name, + agent_type: Some(agent_type.unwrap_or("agent").to_string()), + persona: None, + metadata: None, + }; + let result = relay + .register_or_get_agent(request) + .await + .map_err(relay_error_to_anyhow)?; + return Ok(( + result.id, + result.name, + result.token, + None, // workspace_id not returned in CreateAgentResponse + )); + } + loop { let request = CreateAgentRequest { name: name.clone(), @@ -629,23 +646,6 @@ impl AuthClient { Err(RelayError::Api { code, status, .. }) if is_conflict_code(&code) || status == 409 => { - // strict_name = "I want exactly this name". On 409 we - // reclaim the existing agent via rotate-token so a restart - // — same broker, same workspace key, same cwd-derived - // name — can rejoin instead of fataling (issue #797). - // Without this, sharing a workspace key across machines or - // restarting after a crash kills the broker as soon as the - // cloud still has the prior offline record. When - // strict_name is false the caller is willing to take a - // suffixed name, so we keep the legacy retry. - if strict_name { - return reclaim_agent_via_rotate_token( - workspace_key, - &self.base_url, - &name, - ) - .await; - } if !attempted_retry { attempted_retry = true; let suffix = Uuid::new_v4().simple().to_string(); @@ -768,48 +768,6 @@ fn is_conflict_code(code: &str) -> bool { ) } -/// Reclaim an existing agent on a 409 by rotating its token, returning the -/// fresh `(agent_id, agent_name, token, workspace_id)` tuple. -/// -/// We can't use `relaycast::RelayCast::register_or_get_agent` here because its -/// internal `get_agent` call deserializes into a strict `Agent` struct that -/// doesn't match the live cloud's `GET /v1/agents/{name}` payload (missing -/// `workspace_id`, `token_hash`, `created_at` in the response). Instead we -/// fetch the agent record as `serde_json::Value` and pluck the `id` field — -/// every other field is non-essential for startup. -async fn reclaim_agent_via_rotate_token( - workspace_key: &str, - base_url: &str, - name: &str, -) -> Result<(String, String, String, Option)> { - let relay = build_relay_client(workspace_key, base_url)?; - let token_response = relay - .rotate_agent_token(name) - .await - .map_err(relay_error_to_anyhow) - .with_context(|| format!("failed to rotate token for existing agent '{}'", name))?; - - let http = HttpClient::new(ClientOptions::new(workspace_key).with_base_url(base_url)) - .map_err(|e| anyhow::anyhow!("failed to build http client: {e}"))?; - let agent_record = http - .get::( - &format!("/v1/agents/{}", urlencoding::encode(name)), - None, - None, - ) - .await - .map_err(relay_error_to_anyhow) - .with_context(|| format!("failed to fetch existing agent '{}'", name))?; - - let agent_id = agent_record - .get("id") - .and_then(Value::as_str) - .map(str::to_string) - .with_context(|| format!("agent '{}' record missing 'id' field", name))?; - - Ok((agent_id, token_response.name, token_response.token, None)) -} - fn is_workspace_name_conflict(error: &RelayError) -> bool { match error { RelayError::Api { @@ -957,11 +915,11 @@ mod tests { } #[tokio::test] - async fn strict_name_conflict_reclaims_via_rotate_token() { + async fn strict_name_conflict_reclaims_via_sdk_register_or_get_agent() { // Regression test for issue #797: when a broker is restarted (or a // second broker joins via shared workspace key) with a name that's // already registered, registration must reclaim the existing agent - // via rotate-token instead of failing the broker startup. + // through the relaycast SDK instead of failing the broker startup. let _env_guard = clear_relay_env(); let server = MockServer::start(); unsafe { @@ -985,10 +943,8 @@ mod tests { .header("authorization", "Bearer rk_live_shared"); then.status(200) .header("content-type", "application/json") - // Mirrors the live cloud's GET /v1/agents/{name} payload — - // notably missing workspace_id, token_hash, created_at that - // the relaycast 1.0.0 `Agent` struct expects. Our fix uses - // serde_json::Value to tolerate the shape mismatch. + // Mirrors the live cloud's GET /v1/agents/{name} payload that + // relaycast 1.0.1 accepts while reclaiming the agent. .body(r#"{"ok":true,"data":{"id":"a_existing","name":"lead","type":"agent","status":"offline","persona":null,"metadata":{},"last_seen":"2025-01-01T00:00:00Z","channels":[]}}"#); }); let rotate = server.mock(|when, then| { @@ -1004,7 +960,7 @@ mod tests { let session = client .startup_session_with_options(Some("lead"), true, None) .await - .expect("strict-name conflict should reclaim via rotate-token"); + .expect("strict-name conflict should reclaim via relaycast SDK"); assert_eq!(session.token, "at_live_rotated"); assert_eq!(session.credentials.agent_id, "a_existing");