Skip to content

feat(terminal): add terminal.set_delivery_mode WS frame for --node drive attach - #1495

Merged
khaliqgant merged 9 commits into
mainfrom
agent/fix-broker-node-workspace
Aug 13, 2026
Merged

feat(terminal): add terminal.set_delivery_mode WS frame for --node drive attach#1495
khaliqgant merged 9 commits into
mainfrom
agent/fix-broker-node-workspace

Conversation

@kjgbot

@kjgbot kjgbot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • agent-relay drive --node <agent> was failing with "Error: could not switch '' to auto_inject mode: broker remained in manual_flush mode while draining queued messages" every time
  • Root cause: packages/cli/src/cli/lib/attach-fleet-node.ts had a static stub that always returned manual_flush for drive sessions, ignoring the PUT body — the real broker was never contacted
  • --ssh-host worked because it tunnels directly to the broker's HTTP API; --node goes through a Relaycast WebSocket terminal session where no delivery-mode path existed

Changes

crates/broker/src/terminal_control.rs

  • Added TerminalFromCloud::SetDeliveryMode (client→node) with mode, optional expected_mode, and expected_revision (decimal string, matches HTTP wire format)
  • Added TerminalToCloud::DeliveryMode (node→client) with full CAS reply payload
  • Round-trip serde tests for both variants (including CAS fields)

crates/broker/src/runtime/fleet.rs

  • Handler for SetDeliveryMode: validates session exists and is not view-only, calls self.handle_api_request(ListenApiRequest::SetInboundDeliveryMode { ... }) via oneshot channel — reuses the exact HTTP code path (CAS guards, queue flush on manual_flush → auto_inject, interactive-hold frame, sdk event emission)
  • Replies with DeliveryMode on success or Error on failure (agent_not_found, etc.)

packages/cli/src/cli/lib/attach-fleet-node.ts

  • Replaced static stub with real WS forwarding
  • GET returns locally-tracked loopbackDeliveryMode (updated on each broker reply)
  • PUT: validates mode, checks WS is open, sends terminal.set_delivery_mode frame, awaits terminal.delivery_mode reply (10s timeout)
  • terminal.error while a delivery-mode PUT is in flight routes to the pending request without tearing down the terminal session
  • close() cancels any pending request cleanly

Companion PR: AgentWorkforce/cloud#? (adds terminal.set_delivery_mode/terminal.delivery_mode to the NodeDO passthrough allowlists)

Test plan

  • cargo test --package agent-relay-broker --lib terminal_control — both tests pass
  • TypeScript compiles clean (npx tsc --noEmit in packages/cli)
  • Live test: agent-relay drive --node <agent-name> on a fleet worker — should attach without the manual_flush error

🤖 Generated with Claude Code

kjgbot and others added 2 commits August 13, 2026 00:05
…ive attach

The `--node` drive attach was failing with "broker remained in manual_flush
mode" because attach-fleet-node.ts's delivery-mode proxy handler was a static
stub that always returned `manual_flush` for drive mode, never forwarding PUT
requests to the remote broker.

Root cause: the loopback proxy had no channel to the remote broker's HTTP API —
only a Relaycast WebSocket terminal session. Fix adds a proper WS-level
protocol round-trip:

- broker/terminal_control.rs: add TerminalFromCloud::SetDeliveryMode and
  TerminalToCloud::DeliveryMode frame types (InboundDeliveryMode already has
  snake_case serde serialization; expected_revision is a decimal string to
  match the existing HTTP wire format)

- runtime/fleet.rs: handle SetDeliveryMode by calling handle_api_request with
  a oneshot channel, reusing the existing SetInboundDeliveryMode code path
  (CAS guards, interactive-hold frame, queue flush, sdk event emission); view
  sessions are rejected; reply forwarded as DeliveryMode or Error

- attach-fleet-node.ts: replace stub with real WS forwarding; track
  loopbackDeliveryMode locally (updated on each broker reply); pending
  delivery-mode promise serialises in-flight PUTs; terminal.error while a
  delivery-mode PUT is in flight is routed to the pending request rather than
  tearing down the session; close() cancels any pending request cleanly

Cloud-side DO (node.ts in relaycast) still needs the new frame types added to
its passthrough allowlist — tracked separately (cloud git state is broken,
needs re-clone).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 86760cf5-3838-4db9-ae08-d294db172af7

📥 Commits

Reviewing files that changed from the base of the PR and between 567dec9 and 6dc9e61.

📒 Files selected for processing (4)
  • crates/broker/src/runtime/fleet.rs
  • crates/broker/src/runtime/worker_events.rs
  • packages/cli/src/cli/lib/attach-fleet-node.test.ts
  • packages/cli/src/cli/lib/attach-fleet-node.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli/lib/attach-fleet-node.ts

📝 Walkthrough

Walkthrough

This change forwards delivery-mode updates from the CLI proxy through the terminal WebSocket to the broker. It adds guarded wire messages, broker session handling, response tracking, timeout and shutdown handling, tests, and changelog coverage.

Changes

Delivery-mode forwarding

Layer / File(s) Summary
Terminal delivery-mode protocol
crates/broker/src/terminal_control.rs
Adds guarded terminal.set_delivery_mode requests and terminal.delivery_mode responses. Tests cover optional fields, correlated errors, and compare-and-set results.
Broker delivery-mode handling
crates/broker/src/runtime/fleet.rs, crates/broker/src/runtime/worker_events.rs, crates/broker/src/runtime/maintenance.rs
Handles requests for active writable sessions through the Listen API. Returns delivery-mode results, includes mode state in readiness responses, and adds request correlation fields to terminal errors.
CLI proxy request coordination
packages/cli/src/cli/lib/attach-fleet-node.ts, packages/cli/src/cli/lib/attach-fleet-node.test.ts, CHANGELOG.md
Forwards delivery-mode requests, tracks pending responses, handles timeouts and transport closure, validates results, adds integration tests, and documents the fix.
Trajectory record updates
.agentworkforce/trajectories/...
Records the active implementation trajectory and marks an older trajectory as abandoned.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Mergeability Score: ⚪ Minimal · up to 6dc9e

The PR adds the WebSocket delivery-mode path and associated handling without any identified merge-blocking risk; it is merge-ready after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant CLIClient
  participant attach_fleet_node
  participant terminal_control
  participant fleet_runtime
  participant ListenAPI
  CLIClient->>attach_fleet_node: Request delivery-mode change
  attach_fleet_node->>terminal_control: Send terminal.set_delivery_mode
  terminal_control->>fleet_runtime: Forward delivery-mode request
  fleet_runtime->>ListenAPI: Apply guarded mode change
  ListenAPI-->>fleet_runtime: Return mode, flushed, matched, revision
  fleet_runtime-->>terminal_control: Send terminal.delivery_mode
  terminal_control-->>attach_fleet_node: Resolve pending request
  attach_fleet_node-->>CLIClient: Return broker result
Loading

Possibly related PRs

Suggested reviewers: khaliqgant, willwashburn

Poem

A rabbit sends a mode through the wire,
The broker checks guards as replies rise higher.
The proxy tracks each request in flight,
Then rejects pending work at shutdown night.
“Hop!” says the rabbit, “the mode is now known!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the new WebSocket frame and its target use case for node drive attach sessions.
Description check ✅ Passed The description explains the problem, root cause, implementation, companion dependency, and test plan with the required sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/fix-broker-node-workspace

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 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 12: Update the changelog entry for `agent-relay drive --node` to state
only the user-visible fix: it no longer fails with “broker remained in
manual_flush mode.” Remove references to the Relaycast terminal WebSocket,
remote broker forwarding, and the previous static stub response.

In `@crates/broker/src/runtime/fleet.rs`:
- Around line 447-457: Update the expected_revision parsing in the
SetInboundDeliveryMode request flow to reject any provided value that fails u64
parsing instead of converting it to None. Before calling handle_api_request,
return terminal.error with the invalid_revision error; preserve None only when
expected_revision is absent.

In `@crates/broker/src/terminal_control.rs`:
- Around line 61-69: Correlate delivery-mode requests and replies with a unique
request identifier: in crates/broker/src/terminal_control.rs lines 61-69 add
request_id to SetDeliveryMode, and in lines 115-121 echo it in DeliveryMode and
operation-scoped errors; in crates/broker/src/runtime/fleet.rs lines 425-490
preserve the identifier in success and error replies; in
packages/cli/src/cli/lib/attach-fleet-node.ts lines 302-320 generate and store
an identifier per PUT, and in lines 584-607 settle the pending request only when
the reply identifier matches. Ensure unrelated terminal.error messages cannot
settle the active delivery-mode request.

In `@packages/cli/src/cli/lib/attach-fleet-node.ts`:
- Around line 660-665: Move the pendingDeliveryMode cleanup from the explicit
shutdown path into endTerminal so terminal.closed immediately clears its timer
and rejects the request with the terminal-close error. Update close() to reuse
endTerminal’s cleanup instead of duplicating the logic, preserving the existing
pending request state transitions.
🪄 Autofix

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: 5f972e6a-8564-4099-b56b-f33ffcb72bbf

📥 Commits

Reviewing files that changed from the base of the PR and between 2d7993a and 085aed4.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • crates/broker/src/runtime/fleet.rs
  • crates/broker/src/terminal_control.rs
  • packages/cli/src/cli/lib/attach-fleet-node.ts

Comment thread CHANGELOG.md Outdated
Comment thread crates/broker/src/runtime/fleet.rs Outdated
Comment thread crates/broker/src/terminal_control.rs
Comment thread packages/cli/src/cli/lib/attach-fleet-node.ts Outdated

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

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.

7 issues found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="CHANGELOG.md">

<violation number="1" location="CHANGELOG.md:12">
P3: The new changelog bullet includes implementation backstory ("instead of returning a static stub response") and supervisor mechanism detail, which contradicts the CHANGELOG style rules to drop internal review notes and backstory and keep entries concise and impact-first. The user-visible effect is only that `agent-relay drive --node` no longer fails with the manual_flush error; trim the entry to that effect.</violation>
</file>

<file name="crates/broker/src/runtime/fleet.rs">

<violation number="1" location="crates/broker/src/runtime/fleet.rs:447">
P2: When a terminal frame contains a malformed or overflowing `expected_revision`, this code silently drops the CAS guard and applies the delivery-mode change unconditionally. Reject invalid revisions with a `terminal.error` using `invalid_revision`, matching the HTTP path.</violation>
</file>

<file name="crates/broker/src/terminal_control.rs">

<violation number="1" location="crates/broker/src/terminal_control.rs:65">
P2: When a caller sends an invalid `expected_mode`, serde rejects the entire frame before the runtime can respond, so the request times out instead of returning an invalid-mode error. Validate these fields while preserving a `terminal.error` response for malformed requests.</violation>
</file>

<file name="packages/cli/src/cli/lib/attach-fleet-node.ts">

<violation number="1" location="packages/cli/src/cli/lib/attach-fleet-node.ts:214">
P2: The first GET reports an inferred mode rather than the worker's current mode. When the worker started in the opposite mode, `switchInboundDeliveryModeOrAbort` saves this guess and detach restores the wrong delivery mode; obtain the initial mode through the session protocol or avoid restoring an unverified value.</violation>

<violation number="2" location="packages/cli/src/cli/lib/attach-fleet-node.ts:291">
P2: When the broker reports `agent_not_found` for this PUT, return HTTP 404 instead of always returning 503. The attach preflight relies on 404 to produce the correct no-agent or cross-node placement error.</violation>

<violation number="3" location="packages/cli/src/cli/lib/attach-fleet-node.ts:296">
P2: A delayed broker reply can be lost after the timeout or satisfy a later PUT because delivery-mode frames have no request correlation. Preserve the unknown operation state until the reply is drained, or add request correlation so late replies cannot be applied to another request.</violation>

<violation number="4" location="packages/cli/src/cli/lib/attach-fleet-node.ts:296">
P2: This pendingDeliveryMode cleanup only runs inside close(), but terminal.closed frames call endTerminal, not close(). When the session closes via a terminal.closed frame while a delivery-mode PUT is pending, the request isn't rejected here and instead waits out the full 10s DELIVERY_MODE_TIMEOUT_MS, returning delivery_mode_timeout instead of the actual close error. Move this cleanup into endTerminal and call it from close() as well.</violation>
</file>

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

Re-trigger cubic

Comment thread crates/broker/src/runtime/fleet.rs Outdated
Comment thread crates/broker/src/terminal_control.rs Outdated
Comment thread packages/cli/src/cli/lib/attach-fleet-node.ts
Comment thread packages/cli/src/cli/lib/attach-fleet-node.ts
Comment thread packages/cli/src/cli/lib/attach-fleet-node.ts
Comment thread packages/cli/src/cli/lib/attach-fleet-node.ts
Comment thread CHANGELOG.md Outdated
github-actions Bot and others added 3 commits August 13, 2026 05:24
Addresses all 7 issues from CodeRabbit and Cubic reviews on the
terminal.set_delivery_mode WebSocket frame implementation:

1. invalid_revision: reject unparseable expected_revision with
   terminal.error(invalid_revision) instead of silently dropping the
   CAS guard (which would allow an unconditional delivery-mode change)

2. expected_mode: validate the raw string in fleet.rs and return
   terminal.error(invalid_mode) for unknown values, rather than having
   serde silently drop the whole frame — expected_mode changed to
   Option<String> in the protocol struct

3. request_id correlation: add request_id to SetDeliveryMode,
   DeliveryMode, and Error so delayed broker replies cannot resolve or
   reject a later in-flight PUT; CLI generates a per-PUT ID and
   validates it on terminal.delivery_mode / terminal.error responses

4. endTerminal cleanup: pendingDeliveryMode is now rejected in
   endTerminal so a terminal.closed frame during an in-flight PUT
   rejects it immediately rather than waiting for the 10 s timeout

5. HTTP 404 for agent_not_found: delivery-mode PUT returns 404 instead
   of 503 when the broker reports agent_not_found, matching the attach
   preflight's expected status code for placement errors

6. Initial delivery mode: terminal.ready now includes the worker's
   current delivery_mode from the broker, seeding loopbackDeliveryMode
   with the authoritative value rather than an inferred guess

7. CHANGELOG: trim entry to impact-only per style rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

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.

All reported issues were addressed across 9 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Re-trigger cubic

Comment thread crates/broker/src/runtime/fleet.rs Outdated
Comment thread crates/broker/src/runtime/worker_events.rs

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

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.

All reported issues were addressed across 9 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.

Re-trigger cubic

Comment thread crates/broker/src/runtime/worker_events.rs
Comment thread crates/broker/src/runtime/fleet.rs Outdated
Comment thread packages/cli/src/cli/lib/attach-fleet-node.ts
Four remaining defects across the delivery-mode WS path, verified live
against the current head before fixing:

- crates/broker/src/runtime/fleet.rs: the WS terminal.set_delivery_mode
  handler matched expected_mode against literal "auto_inject"/"manual_flush"
  strings, rejecting a syntactically-valid value with different casing or
  padding (e.g. "MANUAL_FLUSH", " manual_flush ") as invalid_mode, while the
  HTTP delivery-mode route accepted it via InboundDeliveryMode::parse. Extract
  parse_expected_mode() and route both through InboundDeliveryMode::parse so
  WS and HTTP semantics match exactly.

- crates/broker/src/runtime/worker_events.rs: a fresh PTY with no prior
  entry in delivery_states caused the terminal.ready frame to omit the
  worker's delivery mode instead of falling back to the broker's logical
  default (auto_inject), leaving the attach client's own possibly-wrong
  inferred default (manual_flush for --node drive) in place. Extract
  resolve_ready_delivery_mode() and fall back to AutoInject when no state
  entry exists.

- packages/cli/src/cli/lib/attach-fleet-node.ts: extract a shared
  rejectPendingDeliveryMode() helper used by endTerminal, close(), and now
  also the bounded-reconnect close handler. Previously a transport
  disconnect that took the reconnecting path (as opposed to permanent
  teardown) left an in-flight delivery-mode PUT pending — Relaycast drops
  the old lane's terminal session state on disconnect, so the frame already
  sent is lost and a reconnect can never resurrect a reply, guaranteeing a
  10s delivery_mode_timeout on any transient reconnect during a mode
  switch. Reject the pending PUT immediately with a retryable
  delivery_mode_disconnected error instead.

Tests: added packages/cli/src/cli/lib/attach-fleet-node.test.ts (new file —
none existed for this loopback adapter) covering the happy path plus both
prompt-rejection regressions against a real local WebSocketServer standing
in for Relaycast; added Rust unit tests for parse_expected_mode and
resolve_ready_delivery_mode. Confirmed the reconnect-path test times out at
10s without the fix and passes in <1s with it.

cargo fmt --all -- --check, cargo clippy -- -D warnings, cargo test -p
agent-relay-broker (923 passed), npm run lint, npm run typecheck, and
vitest run packages/cli/src (1039 passed) are all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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

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.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/cli/lib/attach-fleet-node.test.ts">

<violation number="1" location="packages/cli/src/cli/lib/attach-fleet-node.test.ts:101">
P3: The PR description lists "close() cancels any pending request cleanly" as delivered behavior (see rejectPendingDeliveryMode in close()), but none of the three tests cover it. The suite covers a successful reply, a terminal.closed rejection, and a transport-disconnect rejection, leaving the close()-cancellation path and its `closed` error code unverified. Add a test that puts a delivery-mode request in flight, calls proxy.close(), and asserts the PUT resolves promptly with 503 / error.code `closed`.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/cli/src/cli/lib/attach-fleet-node.test.ts
khaliqgant and others added 2 commits August 13, 2026 11:30
… PUTs

Cubic flagged (P3, confidence 8) that the delivery-mode PUT test suite
added in 6dc9e61 covered a successful reply, a terminal.closed
rejection, and a transport-disconnect rejection, but not the close()
path itself — the PR description calls out "close() cancels any
pending request cleanly" (rejectPendingDeliveryMode in close()) as
delivered behavior, and that specific path had no direct test.

Add a test that puts a delivery-mode PUT in flight (the remote receives
the frame but deliberately never replies), calls proxy.close(), and
asserts the PUT resolves promptly with 503 / error.code "closed"
rather than hanging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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

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.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/cli/src/cli/lib/attach-fleet-node.test.ts
@khaliqgant
khaliqgant merged commit bbe8b0b into main Aug 13, 2026
45 checks passed
@khaliqgant
khaliqgant deleted the agent/fix-broker-node-workspace branch August 13, 2026 09:43
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