Skip to content

refactor(broker): replace stringly-typed protocol IDs with newtypes#984

Merged
willwashburn merged 4 commits into
mainfrom
claude/practical-hopper-YCUXP
May 25, 2026
Merged

refactor(broker): replace stringly-typed protocol IDs with newtypes#984
willwashburn merged 4 commits into
mainfrom
claude/practical-hopper-YCUXP

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Summary

The agent-relay-broker crate was carrying ~30 protocol fields typed as plain
String whose meaning is well-defined and easy to confuse with one another
(delivery_id vs event_id vs agent_id, name vs target, etc.). The
worst offender was target: String, which carried four distinct meanings —
channel (#general), DM conversation (dm_*), group conversation
(conv_*), worker display name, or the thread-routing sentinel "thread"
— discriminated by hand-rolled prefix checks at every routing call site.

This PR introduces typed wrappers and threads them through. The fix follows
a focused critique that the crate's identifiers were being shuffled around
as raw strings — exactly the kind of stringly-typed sprawl that LLM-written
Rust falls into.

What changed

New module crates/broker/src/ids.rs defines:

  • WorkerName, WorkspaceId, WorkspaceAlias, DeliveryId, EventId,
    ThreadId, AgentId, RequestId, ChannelName#[serde(transparent)]
    newtypes wrapping String.
  • MessageTarget newtype with a kind() method returning a discriminated
    MessageTargetKind { Channel(&str), Thread, DirectMessage(&str), Conversation(&str), Worker(&str) }. The hand-rolled
    event.target.starts_with('#') / event.target == "thread" checks in
    routing.rs become a match on kind().

Each wrapper impls Deref<Target = str>, Display, AsRef<str>,
Borrow<str>, Hash, Eq, From<String> / From<&str>, and
PartialEq<str> / <&str> / <String> (both directions). The point is
that callers that treated these as strings keep compiling — no ceremony at
use sites — but the type system now catches passing a DeliveryId where
an EventId was expected.

The newtypes are applied in:

  • protocol.rs — every variant of BrokerEvent, BrokerToWorker,
    WorkerToBroker, SdkToBroker, plus RelayDelivery, AgentSpec, and
    ProtocolEnvelope.
  • types.rsInboundRelayEvent, PendingRelayMessage,
    BrokerCommandEvent, InjectRequest, SpawnParams, ReleaseParams.
  • listen_api.rs — every variant of ListenApiRequest that carries an
    identifier (Spawn, Release, Send, ResizePty, SubscribeChannels, etc.).
  • runtime/event_loop.rsBrokerRuntime's collection types now key on
    WorkerName / DeliveryId instead of bare String
    (HashMap<WorkerName, InboundDeliveryState>,
    HashMap<DeliveryId, PendingDelivery>,
    HashMap<WorkspaceId, RelayWorkspace>, etc.).
  • runtime/delivery.rsPendingDelivery, PersistedPendingDelivery,
    DeliveryAttemptOutcome, and helper signatures.
  • runtime/session.rs, relaycast/workspace.rsRelayWorkspace,
    WorkspaceSessionHandle, MultiWorkspaceSession,
    WorkspaceMembershipSummary.
  • Other supporting types in worker.rs, wrap.rs, pty_worker.rs,
    broker.rs, runtime/maintenance.rs, runtime/relaycast_events.rs,
    etc.

Why the wire format is preserved

Every newtype is #[serde(transparent)]. JSON on disk and on the SDK ↔
broker channel is byte-for-byte identical to before — both ways, both
directions. The protocol::tests round-trip suite still passes
unchanged.

What is not in this PR

Two follow-ups were scoped but deferred to keep this PR reviewable:

  • The runtime::api::handle_api_request function still pre-binds ~15
    &mut self.field references at its top and is 1500+ lines across 25
    enum arms. Extracting each arm into a focused BrokerRuntime::handle_*
    method is a sizable behavioral refactor of its own.
  • The #[allow(dead_code)] annotations on ~13 modules in lib.rs were
    hiding genuine dead code (scheduler::Scheduler, redact::redact,
    conversation_log::ConversationLog, and others are entirely unused).
    This PR preserves the allows but adds a comment in lib.rs calling
    out that they mask real dead code rather than a library-vs-binary
    visibility artifact, so the follow-up has a starting point.

Test plan

  • cargo test -p agent-relay-broker --lib — 656 passed / 0 failed /
    4 ignored.
  • cargo test -p agent-relay-broker --tests — integration tests
    pass.
  • cargo build -p agent-relay-broker --bin agent-relay-broker — binary
    builds clean.
  • cargo check --all-targets — 0 warnings, 0 errors.
  • Manual smoke: spawn an agent through the HTTP API, send a message
    through a channel target, send a DM, send a thread reply, observe
    that the broker continues to route correctly with the
    MessageTargetKind dispatch in place. (Not exercised locally —
    worth a real-runtime check before merging.)

https://claude.ai/code/session_01QgCfgzy51fdAa6XLRfg3L2


Generated by Claude Code

Introduces nine `#[serde(transparent)]` newtype wrappers in
`crates/broker/src/ids.rs` for the broker's previously string-typed
protocol identifiers — `WorkerName`, `WorkspaceId`, `WorkspaceAlias`,
`DeliveryId`, `EventId`, `ThreadId`, `AgentId`, `RequestId`,
`ChannelName` — plus a `MessageTarget` wrapper whose `kind()` returns a
`MessageTargetKind` enum (`Channel` / `Thread` / `DirectMessage` /
`Conversation` / `Worker`) so the formerly overloaded `target: String`
field is now discriminated structurally instead of by hand-rolled
prefix checks.

Each newtype impls `Deref<Target = str>`, `Display`, `AsRef<str>`,
`Borrow<str>`, `From<String>` / `From<&str>`, `Hash`, `Eq`, and
PartialEq against `str` / `&str` / `String`, so call sites that
treated these fields as strings keep compiling. Wire format is
unchanged — `#[serde(transparent)]` means the JSON shape on disk and
over the SDK ↔ broker channel is byte-for-byte identical to the
previous bare-`String` form.

`protocol.rs`, `types.rs`, `runtime/event_loop.rs`, `listen_api.rs`
and the runtime helpers thread the new types through `RelayDelivery`,
`InboundRelayEvent`, every `BrokerEvent` variant, `PendingDelivery`,
`DeliveryAttemptOutcome`, `ListenApiRequest`, and the
`HashMap<DeliveryId, _>` / `HashMap<WorkerName, _>` collection keys.
Routing's `resolve_delivery_targets` switches from
`event.target.starts_with('#')` / `event.target == "thread"` to a
`MessageTargetKind` match.
@willwashburn willwashburn requested a review from khaliqgant as a code owner May 25, 2026 15:25
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

Review Change Stack

Warning

Review limit reached

@willwashburn, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 4 reviews of capacity. Refill in 6 minutes and 13 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c93e17c-8d59-496f-a72e-94e5695601d7

📥 Commits

Reviewing files that changed from the base of the PR and between 29f93da and 9f55e75.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • crates/broker/src/ids.rs
  • crates/broker/src/runtime/event_loop.rs
  • crates/broker/src/runtime/init.rs
  • crates/broker/src/runtime/worker_events.rs
📝 Walkthrough

Walkthrough

This PR migrates the agent-relay-broker codebase from stringly-typed protocol identifiers to strongly-typed #[serde(transparent)] newtypes (e.g., WorkerName, DeliveryId, EventId, MessageTarget) while preserving wire format compatibility. The refactoring spans the full request/response stack: wire protocol contracts, internal state structures, routing logic, delivery lifecycle management, HTTP API handlers, workspace session management, worker registry, and runtime initialization, with corresponding test updates.

Changes

Typed Identifier Refactoring

Layer / File(s) Summary
Typed identifier infrastructure and public API
crates/broker/src/ids.rs, crates/broker/src/lib.rs, CHANGELOG.md
New ids.rs module defines 10+ typed ID newtypes via string_id! macro with transparent serde, full trait interoperability, and MessageTarget semantic classifier (kind() enum for channel/thread/dm/conv/worker detection). Module is publicly exported via lib.rs.
Wire protocol contracts using typed IDs
crates/broker/src/protocol.rs
AgentSpec, RelayDelivery, ProtocolEnvelope, SdkToBroker, BrokerEvent, and WorkerToBroker message types now use typed ID fields (WorkerName, DeliveryId, EventId, MessageTarget, ThreadId, WorkspaceId, WorkspaceAlias, ChannelName, RequestId) instead of raw strings; serde round-trip tests updated.
Internal message and state types with typed IDs
crates/broker/src/types.rs
PendingRelayMessage, InboundRelayEvent, BrokerCommandEvent, SpawnParams, ReleaseParams, InjectRequest updated to use typed routing/workspace/delivery/event ID fields; serde defaults and queueing behavior preserved.
Broker state, persistence, and verification
crates/broker/src/broker.rs, crates/broker/src/broker/delivery_verification.rs, crates/broker/src/pty_worker.rs
BrokerState agents keyed by WorkerName; PersistedAgent channels as VecChannelName; reap_dead_agents returns VecWorkerName; PendingActivity/PendingVerification/PendingWorkerInjection updated to store delivery_id/event_id/request_id as typed IDs.
Conversation logging with typed targets
crates/broker/src/conversation_log.rs
ConversationLog::log_inbound converts typed MessageTarget to string via as_str() for log entries.
Wrap mode message injection and verification
crates/broker/src/wrap.rs
PendingWrapInjection updated to store event_id/workspace_id/workspace_alias/target as typed IDs; workspace_lookup keys changed to WorkspaceId; delivery_id for echo verification constructed via DeliveryId::new(...).
Message target routing with semantic classification
crates/broker/src/routing.rs
resolve_delivery_targets replaces string-prefix channel detection with MessageTarget::kind() dispatch; thread sentinel constructed via thread_sentinel() helper; RoutingWorker/DeliveryPlan updated to use ChannelName/WorkspaceId/MessageTarget/AgentId; test fixtures updated with typed ID constructors.
Pending delivery state and retry lifecycle
crates/broker/src/runtime/delivery.rs
Pending-delivery maps keyed by DeliveryId instead of String; PendingDelivery/PersistedPendingDelivery worker_name typed as WorkerName; save/load persistence updated to use DeliveryId keys; delivery ack/attempt/retry logic refactored to work with typed IDs; helper functions (drop/take/clear) updated to filter by typed worker/event IDs.
HTTP API request/response contracts and handlers
crates/broker/src/listen_api.rs
ListenApiRequest enum variants (Spawn, Send, Subscribe, inbound delivery-mode routes) updated to use WorkerName/ChannelName/MessageTarget/ThreadId/WorkspaceId/WorkspaceAlias; DeliveryRouteError::WorkerNotFound holds WorkerName; all HTTP handlers construct typed IDs from request paths/query parameters and serialize typed IDs back to JSON strings; test fixtures updated with typed ID constructors.
Relaycast event mapping and command building
crates/broker/src/relaycast/bridge.rs
map_ws_event and map_ws_broker_command populate InboundRelayEvent/BrokerCommandEvent fields using typed ID constructors (EventId::new, WorkspaceId::new, etc.); to_inject_request converts event_id via into_string().
Workspace management and session initialization
crates/broker/src/relaycast/workspace.rs, crates/broker/src/relaycast/ws.rs
MultiWorkspaceSession, WorkspaceInboundMessage, WorkspaceSessionHandle, RelayWorkspace, RelaySession, RelayReadyState updated to use WorkspaceId/WorkspaceAlias/AgentId; workspace credential conversion and http_clients_by_workspace_id keying refactored to use WorkspaceId; WsControl Subscribe/Unsubscribe carry VecChannelName; active_subscriptions returns VecChannelName; ensure_extra_channels accepts &[ChannelName].
Worker identity and lifecycle management
crates/broker/src/worker.rs
WorkerRegistry keyed by WorkerName; WorkerEvent::Message carries WorkerName; spawn accepts WorkspaceId; send_to_worker accepts RequestId; shutdown_all/reap_exited collect/return VecWorkerName; spawn_worker_reader name parameter typed as WorkerName.
Runtime field types and initialization
crates/broker/src/runtime/event_loop.rs, crates/broker/src/runtime/init.rs, crates/broker/src/runtime/io.rs
BrokerRuntime field types: workspace_lookup keyed by WorkspaceId, pending_deliveries keyed by DeliveryId, terminal_failed_deliveries as HashSetEventId, delivery_states keyed by WorkerName, agent_result_tokens stores WorkerName values; initialization populates typed ID collections; send_error/send_frame accept RequestId parameter.
Runtime maintenance, event handling, and worker lifecycle
crates/broker/src/runtime/maintenance.rs, crates/broker/src/runtime/worker_events.rs
Maintenance tick collects due_ids as VecDeliveryId; worker event handler uses event_id (not delivery_id) for terminal_failed_deliveries keying; crash records and restart candidates convert worker names to/from WorkerName.
Supporting utilities, scheduler, and comprehensive test updates
crates/broker/src/runtime/util.rs, crates/broker/src/scheduler.rs, crates/broker/src/supervisor.rs, crates/broker/src/runtime/relaycast_events.rs, crates/broker/src/runtime/spawn_spec.rs, crates/broker/src/runtime/tests.rs
default_spawn_channels returns VecChannelName; Scheduler keys coalesced messages by (String, MessageTarget); relaycast event handler converts worker/channel/target to typed IDs during spawn/release control; all test fixtures and helpers updated to construct typed ID values via newtype constructors.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • AgentWorkforce/relay#907: Directly overlaps on delivery-verification struct field typing and refactoring.
  • AgentWorkforce/relay#908: Updates Relaycast integration layer (bridge, workspace, ws modules) to use crate::ids newtypes in tandem with this PR's type definitions.
  • AgentWorkforce/relay#914: Concurrent updates to broker wire protocol in protocol.rs (event shapes and versioning).

Suggested Reviewers

  • khaliqgant
  • barryollama

🐰 With typed IDs, the protocol sings so clear,
No more string confusion, the contracts are dear,
From WorkerName to MessageTarget's kind,
Strong typing's a joy when it blows your mind!
Serde transparent, the wire stays the same,
Type safety and compat' — now that's the name!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing stringly-typed protocol IDs with newtypes throughout the broker crate.
Description check ✅ Passed The description thoroughly covers the what/why/how with specific examples, test results, and deferred follow-ups, meeting all required template sections.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/practical-hopper-YCUXP

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29f93da8a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

);
if pending_for_failure.is_some() && !delivery_id.is_empty() {
terminal_failed_deliveries.insert(delivery_id.to_string());
terminal_failed_deliveries.insert(EventId::from(&event_id[..]));
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 Track terminal failures by delivery ID

terminal_failed_deliveries is still checked with delivery_id in the delivery_ack guard, but this change now inserts event_id into that set. In the late-ack scenario (delivery_failed arrives first, then a delayed delivery_ack), the guard can no longer match and the ack is processed instead of being ignored, which regresses the intended terminal-state protection for delivery lifecycle events. Store the same identifier that the guard checks (or update the guard to check event IDs consistently).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in cc42f0e. You're right — the original set was keyed by delivery_id and I incorrectly switched the element type to EventId while leaving the guard at line 29 untouched. Reverted the set to HashSet<DeliveryId> and restored delivery_id-based insert/remove so the late-ack guard suppresses correctly again.


Generated by Claude Code

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 31 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/broker/src/runtime/worker_events.rs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/broker/src/runtime/worker_events.rs (1)

21-30: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Terminal-failure guard still uses delivery_id while the set is keyed by event_id.

Line 29 checks terminal_failed_deliveries.contains(delivery_id), but Line 214 stores EventId from event_id. This makes late-ack suppression miss terminal failures, and Line 213’s !delivery_id.is_empty() gate can still insert an empty EventId.

🔧 Suggested fix
-                            let delivery_id = payload
+                            let delivery_id = payload
                                 .get("delivery_id")
                                 .and_then(Value::as_str)
                                 .unwrap_or("");
+                            let event_id = payload
+                                .get("event_id")
+                                .and_then(Value::as_str)
+                                .unwrap_or("");

                             // Terminal guard: ignore late delivery_ack events once a
                             // delivery has reached terminal failed status.
-                            if !delivery_id.is_empty()
-                                && terminal_failed_deliveries.contains(delivery_id)
+                            if !event_id.is_empty()
+                                && terminal_failed_deliveries.contains(event_id)
                             {
                                 tracing::info!(
                                     worker = %name,
                                     delivery_id = %delivery_id,
+                                    event_id = %event_id,
                                     "ignoring late delivery_ack after terminal failed status"
                                 );
                                 return;
                             }
...
-                            if pending_for_failure.is_some() && !delivery_id.is_empty() {
+                            if pending_for_failure.is_some() && !event_id.is_empty() {
                                 terminal_failed_deliveries.insert(EventId::from(&event_id[..]));
                             }

Also applies to: 213-215

🤖 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/runtime/worker_events.rs` around lines 21 - 30, The
terminal-failure guard is checking
terminal_failed_deliveries.contains(delivery_id) while the set is keyed by
EventId derived from the "event_id" payload, causing misses and allowing an
empty delivery_id to be used; change the code to read and validate the
"event_id" (e.g., parse into EventId or a non-empty event_id_str) and use that
event_id when checking terminal_failed_deliveries.contains(...), and ensure you
only insert/check non-empty/valid EventId values (avoid using the empty-string
gate with delivery_id and instead gate on a valid event_id/EventId).
🤖 Prompt for all review comments with 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.

Inline comments:
In `@CHANGELOG.md`:
- Line 17: Update the CHANGELOG entry to call out this Rust API break under a
"Breaking Changes" and "Migration Guidance" heading: state up front that public
structs in agent-relay-broker now require typed ID newtypes (e.g., DeliveryId,
EventId and others defined in protocol.rs and types.rs) instead of String, and
note crates/broker/src/lib.rs now exposes pub mod ids; explain that the wire
format is unchanged but downstream Rust consumers must update code to use the
new newtypes and adjust any usages of MessageTarget::kind() (which now
discriminates Channel/Thread/DirectMessage/Conversation/Worker) rather than
string-prefix checks, and give concise migration steps to replace String IDs
with the corresponding newtypes.

In `@crates/broker/src/ids.rs`:
- Around line 175-177: The doc comment for the string_id! type ChannelName
incorrectly states the value includes a leading `#`, but the protocol uses raw
channel names (e.g., "ops", "alerts") without `#`; update the ChannelName docs
(the string_id! declaration for ChannelName) to state that it represents the raw
channel identifier without a leading `#` and add an example showing the raw form
used on the wire (or mention the wire uses names without `#`) so call sites are
not misled.

---

Outside diff comments:
In `@crates/broker/src/runtime/worker_events.rs`:
- Around line 21-30: The terminal-failure guard is checking
terminal_failed_deliveries.contains(delivery_id) while the set is keyed by
EventId derived from the "event_id" payload, causing misses and allowing an
empty delivery_id to be used; change the code to read and validate the
"event_id" (e.g., parse into EventId or a non-empty event_id_str) and use that
event_id when checking terminal_failed_deliveries.contains(...), and ensure you
only insert/check non-empty/valid EventId values (avoid using the empty-string
gate with delivery_id and instead gate on a valid event_id/EventId).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3147f977-9a13-46a4-a556-e31e7811bf0e

📥 Commits

Reviewing files that changed from the base of the PR and between ff1d4da and 29f93da.

📒 Files selected for processing (31)
  • CHANGELOG.md
  • crates/broker/src/broker.rs
  • crates/broker/src/broker/delivery_verification.rs
  • crates/broker/src/conversation_log.rs
  • crates/broker/src/ids.rs
  • crates/broker/src/lib.rs
  • crates/broker/src/listen_api.rs
  • crates/broker/src/protocol.rs
  • crates/broker/src/pty_worker.rs
  • crates/broker/src/relaycast/bridge.rs
  • crates/broker/src/relaycast/workspace.rs
  • crates/broker/src/relaycast/ws.rs
  • crates/broker/src/routing.rs
  • crates/broker/src/runtime/api.rs
  • crates/broker/src/runtime/delivery.rs
  • crates/broker/src/runtime/event_loop.rs
  • crates/broker/src/runtime/init.rs
  • crates/broker/src/runtime/io.rs
  • crates/broker/src/runtime/maintenance.rs
  • crates/broker/src/runtime/mod.rs
  • crates/broker/src/runtime/relaycast_events.rs
  • crates/broker/src/runtime/session.rs
  • crates/broker/src/runtime/spawn_spec.rs
  • crates/broker/src/runtime/tests.rs
  • crates/broker/src/runtime/util.rs
  • crates/broker/src/runtime/worker_events.rs
  • crates/broker/src/scheduler.rs
  • crates/broker/src/supervisor.rs
  • crates/broker/src/types.rs
  • crates/broker/src/worker.rs
  • crates/broker/src/wrap.rs

Comment thread CHANGELOG.md Outdated
Comment thread crates/broker/src/ids.rs
claude and others added 2 commits May 25, 2026 17:03
The newtype refactor changed the set's element type from
`HashSet<String>` (storing delivery IDs) to `HashSet<EventId>` and
shifted both insert/remove sites to use event IDs. The late-ack guard
at the top of `delivery_ack` handling still reads `delivery_id` off
the payload and queries the set with it, so once the set was keyed by
event IDs the guard's `.contains()` could no longer match and a
late-arriving `delivery_ack` after a terminal `delivery_failed` would
be processed instead of suppressed.

Revert the set's element type to `HashSet<DeliveryId>` and reinstate
`delivery_id`-based insert/remove. The guard, the insert in
`delivery_failed`, and the remove in `delivery_ack` are now all
keyed consistently on the delivery identifier as before this refactor.

Also tighten the `ChannelName` doc comment — channels appear on the
wire as raw names like `"general"` without `#`; the `#` prefix is the
`MessageTarget` convention for routing *to* a channel, not the
channel's own identifier. Restructure the CHANGELOG entry as
`Breaking Changes` plus `Migration Guidance` (per Keep a Changelog +
project changelog rule) since downstream Rust callers must construct
the new typed IDs even though the JSON wire format is unchanged.
@willwashburn willwashburn merged commit 57b0072 into main May 25, 2026
39 checks passed
@willwashburn willwashburn deleted the claude/practical-hopper-YCUXP branch May 25, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants