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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Release workflow changelog generation now writes concise Keep a Changelog sections and skips web-only, release-only, trajectory, PR-review, placeholder, and withdrawn-tag entries.

### Breaking Changes

- `agent-relay-broker`'s public Rust protocol types now require typed ID newtypes (`WorkerName`, `DeliveryId`, `EventId`, `WorkspaceId`, `WorkspaceAlias`, `ThreadId`, `AgentId`, `RequestId`, `ChannelName`, `MessageTarget`) on every protocol struct and enum variant in `protocol.rs`, `types.rs`, and `listen_api.rs::ListenApiRequest`. The new wrappers live in `crates/broker/src/lib.rs` under `pub mod ids`. JSON wire format is unchanged because every wrapper is `#[serde(transparent)]`, so the broker ↔ SDK channel and on-disk persisted state remain byte-compatible.

### Migration Guidance

- Downstream Rust callers must construct identifiers via `relay_broker::ids::{WorkerName, DeliveryId, EventId, MessageTarget, …}` instead of `String`. Each newtype impls `From<String>` / `From<&str>` and `Deref<Target = str>`, so most string-handling code keeps compiling; only construction sites (`HashMap` keys, struct literals, channel sends) need updates.
- Replace ad-hoc target discrimination (`target.starts_with('#')`, `target == "thread"`) with `MessageTarget::kind()` and match on `MessageTargetKind::{Channel, Thread, DirectMessage, Conversation, Worker}`.

### Fixed

- `web`: PR preview SST deploys use and comment the generated CloudFront URL and AWS's managed disabled cache policy instead of creating per-preview Cloudflare DNS records, ACM certificates, and custom CloudFront cache policies.
Expand Down
13 changes: 7 additions & 6 deletions crates/broker/src/broker.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{collections::HashMap, io::Write, path::Path};

use crate::{
ids::{ChannelName, WorkerName},
protocol::{AgentRuntime, AgentSpec},
supervisor::RestartPolicy,
};
Expand All @@ -26,14 +27,14 @@ pub(crate) fn is_pid_alive(pid: u32) -> bool {

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub(crate) struct BrokerState {
pub(crate) agents: HashMap<String, PersistedAgent>,
pub(crate) agents: HashMap<WorkerName, PersistedAgent>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct PersistedAgent {
pub(crate) runtime: AgentRuntime,
pub(crate) parent: Option<String>,
pub(crate) channels: Vec<String>,
pub(crate) channels: Vec<ChannelName>,
#[serde(default)]
pub(crate) pid: Option<u32>,
#[serde(default)]
Expand Down Expand Up @@ -72,8 +73,8 @@ impl BrokerState {
/// Remove persisted agents whose PIDs are no longer alive.
/// Returns the names of agents that were cleaned up.
#[cfg(unix)]
pub(crate) fn reap_dead_agents(&mut self) -> Vec<String> {
let dead: Vec<String> = self
pub(crate) fn reap_dead_agents(&mut self) -> Vec<WorkerName> {
let dead: Vec<WorkerName> = self
.agents
.iter()
.filter(|(_, agent)| {
Expand All @@ -94,9 +95,9 @@ impl BrokerState {
}

#[cfg(not(unix))]
pub(crate) fn reap_dead_agents(&mut self) -> Vec<String> {
pub(crate) fn reap_dead_agents(&mut self) -> Vec<WorkerName> {
// On non-Unix platforms, clear all agents without PID info
let dead: Vec<String> = self
let dead: Vec<WorkerName> = self
.agents
.iter()
.filter(|(_, agent)| agent.pid.is_none())
Expand Down
22 changes: 13 additions & 9 deletions crates/broker/src/broker/delivery_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use serde_json::{json, Value};

use crate::{util::ansi::strip_ansi, worker::detection::ActivityDetector};
use crate::{
ids::{DeliveryId, EventId, MessageTarget, RequestId, WorkspaceAlias, WorkspaceId},
util::ansi::strip_ansi,
worker::detection::ActivityDetector,
};

pub(crate) const ACTIVITY_WINDOW: Duration = Duration::from_secs(5);
pub(crate) const ACTIVITY_BUFFER_MAX_BYTES: usize = 16_000;
Expand Down Expand Up @@ -65,8 +69,8 @@ impl ThrottleState {

#[derive(Debug, Clone)]
pub(crate) struct PendingActivity {
pub delivery_id: String,
pub event_id: String,
pub delivery_id: DeliveryId,
pub event_id: EventId,
pub expected_echo: String,
pub verified_at: Instant,
pub output_buffer: String,
Expand All @@ -85,18 +89,18 @@ pub(crate) const VERIFICATION_WINDOW: std::time::Duration = std::time::Duration:
/// A pending delivery waiting for echo verification in PTY output.
#[derive(Debug)]
pub(crate) struct PendingVerification {
pub delivery_id: String,
pub event_id: String,
pub delivery_id: DeliveryId,
pub event_id: EventId,
pub expected_echo: String,
pub injected_at: std::time::Instant,
pub attempts: usize,
pub max_attempts: usize,
pub request_id: Option<String>,
pub workspace_id: Option<String>,
pub workspace_alias: Option<String>,
pub request_id: Option<RequestId>,
pub workspace_id: Option<WorkspaceId>,
pub workspace_alias: Option<WorkspaceAlias>,
pub from: String,
pub body: String,
pub target: String,
pub target: MessageTarget,
}

/// Check if the expected echo string appears in PTY output (after stripping ANSI).
Expand Down
2 changes: 1 addition & 1 deletion crates/broker/src/conversation_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl ConversationLog {
let msg_type = match event.kind {
crate::types::InboundKind::DmReceived => "DM".to_string(),
crate::types::InboundKind::GroupDmReceived => "Group DM".to_string(),
crate::types::InboundKind::MessageCreated => event.target.clone(),
crate::types::InboundKind::MessageCreated => event.target.as_str().to_string(),
crate::types::InboundKind::ThreadReply => format!(
"Thread {}",
short_id(event.thread_id.as_deref().unwrap_or("?"))
Expand Down
Loading
Loading