Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/broker/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ serde_json = "1.0"
sha2 = "0.10"
shlex = "1.3"
thiserror = "2.0"
relaycast = "=2.0.0"
relaycast = "=2.3.0"
tokio = { version = "1.44", features = ["full"] }
tracing = "0.1"
tracing-appender = "0.2"
Expand Down
5 changes: 4 additions & 1 deletion crates/broker/src/relaycast/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,10 @@ fn auth_http_status(err: &anyhow::Error) -> Option<StatusCode> {

/// Build a `RelayCast` workspace client from an API key and base URL.
fn build_relay_client(api_key: &str, base_url: &str) -> Result<RelayCast> {
let opts = RelayCastOptions::new(api_key).with_base_url(base_url);
let mut opts = RelayCastOptions::new(api_key).with_base_url(base_url);
if let Some(harness) = crate::telemetry::orchestrator_harness_opt() {
opts = opts.with_harness(harness);
}
RelayCast::new(opts).map_err(|e| anyhow::anyhow!("{e}"))
}

Expand Down
23 changes: 17 additions & 6 deletions crates/broker/src/relaycast/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,11 @@ mod tests {

#[test]
fn maps_presence_top_level() {
// v8 contract: presence transitions are `agent.status.active` /
// `agent.status.offline` (the gateway's engine), not the pre-v8
// `agent.online`. See engine presence adapter.
let event = map_event(&json!({
"type": "agent.online",
"type": "agent.status.active",
"agent": { "name": "alice" }
}))
.unwrap();
Expand Down Expand Up @@ -418,14 +421,18 @@ mod tests {

#[test]
fn maps_reaction_added() {
// v8 contract: reactions arrive as a single `message.reacted` event
// (with an `action` of "added"/"removed"), not the pre-v8
// `reaction.added`/`reaction.removed`. See engine reaction route.
let event = map_event(&json!({
"type": "reaction.added",
"type": "message.reacted",
"action": "added",
"message_id": "msg_42",
"emoji": "thumbsup",
"agent_name": "alice",
"channel_name": "general"
}))
.expect("should map reaction.added");
.expect("should map message.reacted");

assert_eq!(event.kind, InboundKind::ReactionReceived);
assert_eq!(event.from, "alice");
Expand All @@ -439,14 +446,17 @@ mod tests {

#[test]
fn maps_reaction_removed() {
// A reaction removal is the same `message.reacted` event with
// `action: "removed"`; the broker surfaces both as ReactionReceived.
let event = map_event(&json!({
"type": "reaction.removed",
"type": "message.reacted",
"action": "removed",
"message_id": "msg_42",
"emoji": "thumbsup",
"agent_name": "bob",
"channel_name": "dev"
}))
.expect("should map reaction.removed");
.expect("should map message.reacted");

assert_eq!(event.kind, InboundKind::ReactionReceived);
assert_eq!(event.from, "bob");
Expand All @@ -458,7 +468,8 @@ mod tests {
// Reactions without a channel (e.g. DM reactions) are dropped from PTY
// injection — they're surfaced via the inbox API / piggyback instead.
assert!(map_event(&json!({
"type": "reaction.added",
"type": "message.reacted",
"action": "added",
"message_id": "msg_99",
"emoji": "rocket",
"agent_name": "carol"
Expand Down
18 changes: 13 additions & 5 deletions crates/broker/src/relaycast/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,15 @@ impl RelaycastWsClient {
);
}

let mut ws = WsClient::new(
WsClientOptions::new(self.workspace_http.api_key.clone())
.with_base_url(self.ws_base_url.clone()),
);
let mut ws_opts = WsClientOptions::new(self.workspace_http.api_key.clone())
.with_base_url(self.ws_base_url.clone());
// Forward the detected harness as the `harness` query param (WS
// upgrades can't set custom headers) so the backend can attribute
// server events to claude-code / codex / etc. instead of "unknown".
if let Some(harness) = crate::telemetry::orchestrator_harness_opt() {
ws_opts = ws_opts.with_harness(harness);
}
let mut ws = WsClient::new(ws_opts);

match ws.connect().await {
Ok(()) => {
Expand Down Expand Up @@ -769,7 +774,10 @@ impl RelaycastHttpClient {

/// Build a `RelayCast` workspace client from an API key and base URL.
fn build_relay_client(api_key: &str, base_url: &str) -> Option<RelayCast> {
let opts = RelayCastOptions::new(api_key).with_base_url(base_url);
let mut opts = RelayCastOptions::new(api_key).with_base_url(base_url);
if let Some(harness) = crate::telemetry::orchestrator_harness_opt() {
opts = opts.with_harness(harness);
}
RelayCast::new(opts).ok()
}

Expand Down
33 changes: 32 additions & 1 deletion crates/broker/src/spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ pub fn spawn_env_vars(
channels: &str,
workspaces_json: Option<&str>,
default_workspace: Option<&str>,
harness: Option<&str>,
) -> Vec<(String, String)> {
let mut env = vec![
("RELAY_AGENT_NAME".to_string(), name.to_string()),
Expand All @@ -232,6 +233,12 @@ pub fn spawn_env_vars(
("RELAY_CHANNELS".to_string(), channels.to_string()),
("RELAY_STRICT_AGENT_NAME".to_string(), "1".to_string()),
];
// Forward the detected harness so spawned agents using the JS SDK
// (`@relaycast/sdk`, which resolves harness from env and has no
// process-tree fallback) report it to the backend instead of "unknown".
if let Some(harness) = harness {
env.push(("AGENT_RELAY_HARNESS".to_string(), harness.to_string()));
}
if let Some(workspaces_json) = workspaces_json
.map(str::trim)
.filter(|value| !value.is_empty())
Expand Down Expand Up @@ -270,7 +277,31 @@ mod tests {
use nix::unistd::{getsid, Pid};
use tokio::process::Command;

use super::{terminate_child, Spawner};
use super::{spawn_env_vars, terminate_child, Spawner};

#[test]
fn spawn_env_vars_forwards_harness_when_present() {
let env = spawn_env_vars(
"a",
"rk_live_x",
"https://gw",
"#c",
None,
None,
Some("codex"),
);
let harness = env
.iter()
.find(|(k, _)| k == "AGENT_RELAY_HARNESS")
.map(|(_, v)| v.as_str());
assert_eq!(harness, Some("codex"));
}

#[test]
fn spawn_env_vars_omits_harness_when_absent() {
let env = spawn_env_vars("a", "rk_live_x", "https://gw", "#c", None, None, None);
assert!(env.iter().all(|(k, _)| k != "AGENT_RELAY_HARNESS"));
}

#[tokio::test]
async fn release_terminates_child_process() {
Expand Down
21 changes: 20 additions & 1 deletion crates/broker/src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,25 @@ fn detect_orchestrator_harness() -> String {
.unwrap_or_else(|| UNKNOWN_ORCHESTRATOR_HARNESS.to_string())
}

/// Process-wide cached orchestrator harness: explicit env override, else
/// process-tree detection (claude-code / codex / cursor / …). Detection walks
/// the parent-process chain, so we resolve it once and reuse the result for
/// both our own PostHog events and the harness we forward to the relaycast
/// backend. Returns the [`UNKNOWN_ORCHESTRATOR_HARNESS`] sentinel when
/// undetectable.
pub(crate) fn orchestrator_harness() -> &'static str {
static CACHE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
CACHE.get_or_init(detect_orchestrator_harness)
}

/// Like [`orchestrator_harness`] but `None` instead of the `"unknown"`
/// sentinel, so callers can skip forwarding a non-informative value (the
/// relaycast backend already defaults a missing harness to `"unknown"`).
pub(crate) fn orchestrator_harness_opt() -> Option<&'static str> {
let harness = orchestrator_harness();
(harness != UNKNOWN_ORCHESTRATOR_HARNESS).then_some(harness)
}
Comment on lines +440 to +457

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include AGENT_RELAY_HARNESS in broker-side harness detection.

orchestrator_harness_opt() now drives relaycast forwarding, but the detector path it wraps does not check AGENT_RELAY_HARNESS. If that key is the only explicit override, Line 455 resolves to inferred/"unknown" and drops the intended value.

Suggested patch
 fn detect_orchestrator_harness() -> String {
     for key in [
+        "AGENT_RELAY_HARNESS",
         ORCHESTRATOR_HARNESS_ENV,
         "RELAYCAST_HARNESS",
         "X_RELAYCAST_HARNESS",
     ] {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/broker/src/telemetry.rs` around lines 440 - 457, The detector
currently cached by orchestrator_harness() doesn't consider the
AGENT_RELAY_HARNESS environment override, so orchestrator_harness_opt() can drop
a valid explicit value; update the detection path (the
detect_orchestrator_harness function called by orchestrator_harness()) to check
for AGENT_RELAY_HARNESS first (like any other explicit env override) and return
that value when present so the cached String includes the override and
orchestrator_harness_opt() will preserve it.


/// Best-effort OS release string for telemetry tagging. Shells out to
/// `uname -r` on unix (broker is unix-only anyway); returns `None` on
/// failure so we just omit the property rather than risking a crash.
Expand Down Expand Up @@ -562,7 +581,7 @@ impl TelemetryClient {
cli_version: env_nonempty("AGENT_RELAY_CLI_VERSION"),
sdk_version: env_nonempty("AGENT_RELAY_SDK_VERSION"),
os_version: detect_os_version(),
orchestrator_harness: detect_orchestrator_harness(),
orchestrator_harness: orchestrator_harness().to_string(),
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/broker/src/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,7 @@ pub(crate) async fn run_wrap(
&channels,
Some(&child_workspaces_json),
default_workspace_id.as_deref(),
crate::telemetry::orchestrator_harness_opt(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive spawned agents' harness from their CLI

For spawn actions where the requested child CLI differs from the broker's parent harness (for example a Codex-driven broker spawning claude, or a broker launched from a service with no detectable parent spawning codex), this passes the broker's orchestrator harness or None into the child's environment. The JS SDK treats AGENT_RELAY_HARNESS as the spawned process's request harness, so those child Relaycast events are now misattributed to the parent or remain unknown even though params.cli identifies the actual child harness.

Useful? React with 👍 / 👎.

);
// Pre-register the child agent so its MCP server
// starts with a valid token (avoiding "Not registered"
Expand Down
Loading