Skip to content

feat(cli): attach terminals through fleet nodes - #1484

Merged
khaliqgant merged 9 commits into
mainfrom
feat/node-terminal-attach-1449
Aug 11, 2026
Merged

feat(cli): attach terminals through fleet nodes#1484
khaliqgant merged 9 commits into
mainfrom
feat/node-terminal-attach-1449

Conversation

@miyaontherelay

Copy link
Copy Markdown
Contributor

Closes #1449

Depends on cloud #2995.

Implements the Relay half of canonical --node terminal attach:

  • a separate authenticated outbound terminal WebSocket, never multiplexed with heartbeats/actions
  • bounded terminal output/input queues, server-enforced view mode defense in depth, reconnect-friendly session handling
  • remote ANSI snapshots and output routing from PTY workers
  • ephemeral loopback broker-compatible adapter preserving view, drive, and passthrough; no SSH process is used by --node
  • keeps feat(cli): add explicit SSH fallback for remote agent attach #1483 --ssh-host path untouched and mutually exclusive

Checks: cargo fmt --check; cargo check -p agent-relay-broker; cargo test -p agent-relay-broker terminal_wire_round_trips_without_control_frames --lib. CLI focused Vitest launched: packages/cli/src/cli/commands/local-agent.test.ts.

@coderabbitai

coderabbitai Bot commented Aug 11, 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
📝 Walkthrough

Walkthrough

Adds authenticated --node terminal attachment for physical and Daytona fleet nodes. The CLI uses a bounded loopback broker proxy and a dedicated terminal WebSocket. The broker manages sessions, PTY snapshots, input, resizing, output, reconnects, and shutdown. --ssh-host remains the physical-node fallback.

Changes

Fleet node terminal attachment

Layer / File(s) Summary
Terminal protocol and reconnecting transport
crates/broker/src/terminal_control.rs, crates/broker/src/lib.rs, crates/broker/src/runtime/mod.rs
Defines terminal messages, authentication, bounded channels, reconnect handling, shutdown, and wire-format tests.
Broker terminal session handling
crates/broker/src/runtime/init.rs, crates/broker/src/runtime/event_loop.rs, crates/broker/src/runtime/fleet.rs, crates/broker/src/runtime/worker_events.rs, crates/broker/src/runtime/api.rs, crates/broker/src/runtime/maintenance.rs
Starts the terminal lane and handles sessions, PTY operations, worker output, request expiry, queue failures, and worker cleanup.
Fleet attach loopback proxy
packages/cli/src/cli/lib/attach-fleet-node.ts
Creates authenticated terminal sessions and exposes broker-compatible HTTP and WebSocket endpoints with buffering, replay, reconnect, and shutdown handling.
CLI node attach dispatch
packages/cli/src/cli/commands/local-agent.ts, packages/cli/src/cli/commands/local-agent.test.ts, CHANGELOG.md
Adds --node, validates incompatible options, dispatches view, drive, and passthrough attachment, tests errors and routing, and documents the SSH fallback.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant LocalAgent
  participant FleetNodeProxy
  participant BrokerRuntime
  participant Worker

  User->>LocalAgent: run attach --node
  LocalAgent->>FleetNodeProxy: startFleetNodeAttachProxy
  FleetNodeProxy->>BrokerRuntime: create authenticated terminal session
  BrokerRuntime->>Worker: request PTY snapshot
  Worker-->>BrokerRuntime: return snapshot and stream output
  BrokerRuntime-->>FleetNodeProxy: send terminal messages
  FleetNodeProxy-->>LocalAgent: expose broker-compatible terminal traffic
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: willwashburn, khaliqgant

Poem

A rabbit hops across the node,
A terminal carries its load.
Snapshots start and outputs flow,
Bounded streams reconnect and grow.
--node opens the remote way;
SSH remains for fallback play.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: terminal attachment through fleet nodes.
Description check ✅ Passed The description explains the implementation, scope, dependencies, and test commands, but omits the template headings and checkbox format.
Linked Issues check ✅ Passed The changes address issue #1449 by adding remote node attachment, preserving modes, enforcing view safety, and reporting terminal failures.
Out of Scope Changes check ✅ Passed The changes remain focused on fleet-node terminal attachment, broker transport, CLI integration, testing, and related documentation.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/node-terminal-attach-1449

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: 13

🧹 Nitpick comments (5)
crates/broker/src/runtime/worker_events.rs (1)

6-29: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Add an early return for the common no-session case.

This function runs for every PTY chunk from every worker. Most brokers have no terminal session attached, yet each call still iterates the map and builds a Vec. When a session does exist, chunk is cloned once per session even though the last send could take ownership.

An if terminal_sessions.is_empty() { return; } guard at the top removes the scan on the hot path at no cost.

🤖 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 6 - 29, Add an early
return at the start of publish_terminal_output when terminal_sessions is empty,
before collecting session IDs, so the common no-session path avoids scanning and
allocating while preserving existing behavior for attached sessions.
crates/broker/src/terminal_control.rs (2)

218-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extend the wire round-trip test to the remaining message variants.

The test covers terminal.open decoding and terminal.output encoding. The CLI also sends terminal.input, terminal.resize, and terminal.close, and consumes terminal.ready, terminal.input_ack, terminal.error, and terminal.closed. A rename or field change in any of those variants would break the CLI without failing a test. Add assertions for each variant tag and for the skip_serializing_if behaviour on the optional offset, code, and message fields.

🤖 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/terminal_control.rs` around lines 218 - 244, Extend
terminal_wire_round_trips_without_control_frames to cover decoding or encoding
for terminal.input, terminal.resize, terminal.close, terminal.ready,
terminal.input_ack, terminal.error, and terminal.closed, asserting each expected
wire tag. Also verify optional offset, code, and message fields are omitted when
absent and included when present, preserving the existing terminal.open and
terminal.output checks.

186-215: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Send a WebSocket close frame before shutdown.

When TerminalControlCommand::Shutdown or command_rx.recv() returns None, send Message::Close(None) before returning.

🤖 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/terminal_control.rs` around lines 186 - 215, Update the
TerminalControlCommand::Shutdown and command_rx.recv() == None branch in the
terminal control loop to send a WebSocket Message::Close(None) through sink
before returning. Preserve the existing immediate return behavior after the
close-frame send.
packages/cli/src/cli/commands/local-agent.test.ts (1)

99-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the remaining --node branches.

Two branches in the --node dispatch have no test:

  1. The broker-override rejection at packages/cli/src/cli/commands/local-agent.ts lines 553-564 (--node combined with --broker-url, --api-key, or --state-dir).
  2. The failure path at lines 572-575, where deps.attachNode throws and the command must report the message and exit with 1.

Both are small additions that follow the shape of the --ssh-host conflict test below.

Note also that this test asserts expect.objectContaining({ json: true }) reaches attachNode, but the default attachNode implementation discards those flags. See the separate comment on local-agent.ts lines 85-107.

🤖 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 `@packages/cli/src/cli/commands/local-agent.test.ts` around lines 99 - 113, Add
tests for the remaining --node dispatch branches in the local-agent command:
verify broker-related options (--broker-url, --api-key, or --state-dir) are
rejected with the same conflict behavior as the --ssh-host tests, and verify an
attachNode failure reports its error message and exits with status 1. Extend the
harness as needed to make deps.attachNode throw, while preserving the existing
successful --node assertions.
crates/broker/src/runtime/init.rs (1)

309-315: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a deeper terminal queue than the control queue.

The terminal channel uses the same depth (256) as fleet_control. The two lanes have very different traffic shapes: node control carries occasional heartbeats and actions, while the terminal lane carries every PTY chunk for every attached session. publish_terminal_output in crates/broker/src/runtime/worker_events.rs ends the session when try_send fails, so a short output burst from a verbose agent can terminate a healthy attach.

Raising the depth (or reading it from an env var next to node_max_agents) keeps the bound while making a transient burst survivable.

🤖 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/init.rs` around lines 309 - 315, Increase the
bounded capacity of the terminal control and event channels created in the
runtime initialization block, keeping the fleet/control queue at its existing
depth. Choose a deeper fixed capacity or reuse an environment-configured value
alongside node_max_agents, while preserving the existing publish_terminal_output
behavior and bounded-queue semantics.
🤖 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 trailing clause of the adjacent --ssh-host changelog
bullet to describe --node as supporting the implemented authenticated terminal
attach behavior, removing the contradictory statement that it remains reserved
for canonical fleet-native attach. Preserve the concise impact-first style and
the existing release-section structure.

In `@crates/broker/src/runtime/fleet.rs`:
- Around line 111-142: Remove the inserted terminal_sessions entry for
session_id when send_to_worker returns Err in the PTY snapshot path, before
emitting snapshot_failed. In event_loop.rs
(crates/broker/src/runtime/event_loop.rs:207-215), add a WorkerName cleanup
helper that removes matching terminal_sessions and terminal_snapshot_requests
entries, invoke it alongside resize_owners.remove and pty_observability.remove
in worker release and reap paths, and add deadline-based cleanup of
terminal_snapshot_requests in the reap_tick arm.
- Around line 166-176: Update the BASE64 decoding branch in the terminal input
handler to reject data_base64 whose encoded length exceeds the safe bound of 64
* 1024 * 4 / 3 + 4 before calling BASE64.decode. Preserve the existing
invalid_input terminal error and return behavior, then retain the decoded-length
check as a defense-in-depth validation.

In `@crates/broker/src/runtime/init.rs`:
- Around line 249-251: Ensure terminal URL derivation in the initialization flow
fails loudly when fleet_ws_url does not contain the expected control path.
Update the logic around terminal_ws_url to assert that the substitution
occurred, or reuse a relaycast node_terminal_ws_url helper if available, while
preserving the requirement that terminal traffic uses a distinct websocket
endpoint.

In `@crates/broker/src/runtime/worker_events.rs`:
- Around line 944-960: Update the terminal output handling around
publish_terminal_output to reuse the chunk and offset values already extracted
when constructing stream_event, rather than rereading payload fields. Only call
publish_terminal_output when the extracted chunk is non-empty, while preserving
the existing offset and terminal session arguments.
- Around line 860-885: Update the terminal snapshot parsing that constructs
TerminalToCloud::Ready so offset is optional and defaults to 0 when absent,
while keeping screen, rows, and cols required for a valid snapshot. Preserve the
existing malformed-response Error path when any mandatory field is missing or
invalid.

In `@crates/broker/src/terminal_control.rs`:
- Around line 151-159: Update the invalid token-header branch in the terminal
reconnect loop to multiply reconnect_delay before sleeping and continuing,
matching the backoff behavior of the other failure arms. Preserve the existing
warning and retry flow.

In `@packages/cli/src/cli/commands/local-agent.ts`:
- Around line 572-575: Update the error handling in the catch block of the
local-agent command to always emit a consistent “Error: ” prefix, including for
plain Error instances, while avoiding a duplicated prefix when the message
already begins with it. Preserve the existing non-Error string conversion and
exit behavior, and keep the change scoped to the deps.error call.

In `@packages/cli/src/cli/lib/attach-fleet-node.ts`:
- Around line 283-288: The output replay loop must not silently discard events
when the socket closes or exceeds MAX_BUFFERED_BYTES. Update the outputHistory
handling around workerStreamEvent to retain the unsent remainder, or emit a
pty_input_error with an explicit truncation code before clearing it, so users
are informed when replay output is incomplete.
- Around line 167-168: Authenticate the loopback server created in
attach-fleet-node.ts by minting a random token at startup, passing it to attach
clients through brokerUrl/apiKey, and rejecting requests that lack the token
before handling HTTP or WebSocket routes. Apply this to the request flow around
createServer and the /api/input/<agent>/stream endpoint, while preserving the
broker’s existing view-mode input rejection.
- Around line 66-86: Update readBody to settle its promise when the
IncomingMessage emits either 'error' or 'aborted', resolving with the same
empty-object fallback used for invalid JSON. Preserve the existing data
accumulation and end-handler parsing behavior while ensuring await
readBody(request) cannot remain pending after a request failure.
- Around line 169-188: Update the snapshot wait logic in the GET `/snapshot`
handler to retain the timeout handle and clear it whenever `ready` settles,
including when it wins the `Promise.race`; preserve the existing timeout error
response and successful snapshot response behavior.
- Around line 385-402: Update the terminal websocket reconnect flow around the
socket error/close handlers and terminal.ready processing to track
reconnectAttempts, apply exponential backoff capped at 30 seconds, and stop
reconnecting after a bounded maximum. When a resumed connection fails, surface
the failure instead of ignoring it, and reject the attach with a
FleetNodeAttachError using node_unreachable once the cap is reached. Reset
reconnectAttempts to zero when a terminal.ready frame is received, while
preserving the existing reconnect guards and stopped-socket checks.

---

Nitpick comments:
In `@crates/broker/src/runtime/init.rs`:
- Around line 309-315: Increase the bounded capacity of the terminal control and
event channels created in the runtime initialization block, keeping the
fleet/control queue at its existing depth. Choose a deeper fixed capacity or
reuse an environment-configured value alongside node_max_agents, while
preserving the existing publish_terminal_output behavior and bounded-queue
semantics.

In `@crates/broker/src/runtime/worker_events.rs`:
- Around line 6-29: Add an early return at the start of publish_terminal_output
when terminal_sessions is empty, before collecting session IDs, so the common
no-session path avoids scanning and allocating while preserving existing
behavior for attached sessions.

In `@crates/broker/src/terminal_control.rs`:
- Around line 218-244: Extend terminal_wire_round_trips_without_control_frames
to cover decoding or encoding for terminal.input, terminal.resize,
terminal.close, terminal.ready, terminal.input_ack, terminal.error, and
terminal.closed, asserting each expected wire tag. Also verify optional offset,
code, and message fields are omitted when absent and included when present,
preserving the existing terminal.open and terminal.output checks.
- Around line 186-215: Update the TerminalControlCommand::Shutdown and
command_rx.recv() == None branch in the terminal control loop to send a
WebSocket Message::Close(None) through sink before returning. Preserve the
existing immediate return behavior after the close-frame send.

In `@packages/cli/src/cli/commands/local-agent.test.ts`:
- Around line 99-113: Add tests for the remaining --node dispatch branches in
the local-agent command: verify broker-related options (--broker-url, --api-key,
or --state-dir) are rejected with the same conflict behavior as the --ssh-host
tests, and verify an attachNode failure reports its error message and exits with
status 1. Extend the harness as needed to make deps.attachNode throw, while
preserving the existing successful --node assertions.
🪄 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: 3650f50e-e0a3-416e-b229-9964b9b812ab

📥 Commits

Reviewing files that changed from the base of the PR and between ed8144c and 6244479.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • crates/broker/src/lib.rs
  • crates/broker/src/runtime/event_loop.rs
  • crates/broker/src/runtime/fleet.rs
  • crates/broker/src/runtime/init.rs
  • crates/broker/src/runtime/mod.rs
  • crates/broker/src/runtime/worker_events.rs
  • crates/broker/src/terminal_control.rs
  • packages/cli/src/cli/commands/local-agent.test.ts
  • packages/cli/src/cli/commands/local-agent.ts
  • packages/cli/src/cli/lib/attach-fleet-node.ts

Comment thread CHANGELOG.md Outdated
Comment thread crates/broker/src/runtime/fleet.rs
Comment thread crates/broker/src/runtime/fleet.rs
Comment thread crates/broker/src/runtime/init.rs Outdated
Comment thread crates/broker/src/runtime/worker_events.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 Outdated
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.

All reported issues were addressed

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

Comment thread crates/broker/src/runtime/fleet.rs Outdated
Comment thread crates/broker/src/runtime/fleet.rs Outdated
Comment thread crates/broker/src/runtime/event_loop.rs
Comment thread crates/broker/src/runtime/worker_events.rs Outdated
Comment thread crates/broker/src/runtime/fleet.rs
Comment thread crates/broker/src/runtime/init.rs
Comment thread crates/broker/src/runtime/fleet.rs
Comment thread CHANGELOG.md Outdated
Comment thread packages/cli/src/cli/commands/local-agent.test.ts Outdated
Comment thread crates/broker/src/terminal_control.rs
Comment thread packages/cli/src/cli/lib/attach-fleet-node.ts
@khaliqgant

Copy link
Copy Markdown
Member

Independent code review (workflow wf_9ec0d5b5-582, high effort, 16 agents) found 10 distinct defects, 9 CONFIRMED + 1 PLAUSIBLE. Holding pending fixes, ranked by severity:

Correctness (fix before merge):

  1. fleet.rs:137 — on terminal.open, the session is inserted into terminal_sessions before dispatch, but the Err branch (worker channel saturated/closed) never removes it. If the client's terminal.close never arrives, the entry leaks forever and every subsequent worker_stream chunk for that agent wastes a slot in the bounded 256-message queue.
  2. attach-fleet-node.ts:178 — the loopback proxy's snapshot route always answers failures with HTTP 503, collapsing the 404/409 contract captureAndRenderSnapshot needs to hard-abort. A nonexistent or headless agent no longer errors cleanly like local/--ssh-host attach do — it hangs on a permanently blank terminal instead.
  3. local-agent.ts:549--json/--reasoning/--diagnostics are silently discarded on the --node path (void options;) while --ssh-host forwards them. Running --node --json produces no NDJSON and no warning.
  4. terminal_control.rs:152 — the invalid-Authorization-header branch never doubles reconnect_delay, unlike its two sibling failure branches. A corrupted/rotated token gets stuck retrying every 1s forever instead of backing off to 30s.
  5. fleet.rs:83 (PLAUSIBLE) — the Disconnected arm only logs; it never clears/resyncs terminal_sessions/terminal_snapshot_requests even though the underlying websocket identity changes on reconnect. If Relaycast invalidates session state on the old connection, frames enqueued on the new one are silently unroutable — a dark terminal with no close/error signal.

Cleanup (confirmed, lower severity, at your discretion):
6. fleet.rs:134 — the snapshot-request tracking map reimplements the existing worker_request pending/reap pattern by hand and is never reaped by the maintenance tick — leaks on every hung request.
7. fleet.rs:816 — agent release clears resize_owners/pty_observability but not the two new terminal maps; a respawned agent under the same name inherits stale terminal state.
8. attach-fleet-node.ts:115 — a full hand-maintained broker HTTP/WS emulator (7+ endpoints) with no shared contract against the real broker; will silently drift when the real broker's shape changes.
9. local-agent.ts:546 — three copy-pasted mutual-exclusion checks, already inconsistent (the --ssh-host check omits --state-dir, the --node check includes it).
10. CHANGELOG.md:12 — entry includes implementation backstory this repo's own AGENTS.md changelog rule says to drop.

Requesting fixes for #1-5 before this goes to Khaliq for the required review. #6-10 at your discretion given time.

@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: 2

🤖 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 `@crates/broker/src/runtime/event_loop.rs`:
- Around line 280-285: Update TerminalSession readiness to track the active
connection generation, and reset or invalidate it when fleet.rs handles
disconnect/reconnect rather than preserving ready=true. On each new connection,
replay a Ready snapshot for every retained session before releasing queued
Output, and discard or gate commands belonging to the prior connection. Add a
disconnect/reconnect test asserting Ready precedes all resumed output.

In `@packages/cli/src/cli/commands/local-agent.test.ts`:
- Line 146: Update the test around the error mock assertion to explicitly verify
that error was called exactly once, while retaining the existing
toHaveBeenCalledWith check for the expected message.
🪄 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: 6666821d-37e1-4a14-af55-d18a266c543c

📥 Commits

Reviewing files that changed from the base of the PR and between 113e089 and 4771be7.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • crates/broker/src/runtime/api.rs
  • crates/broker/src/runtime/event_loop.rs
  • crates/broker/src/runtime/fleet.rs
  • crates/broker/src/runtime/init.rs
  • crates/broker/src/runtime/maintenance.rs
  • crates/broker/src/runtime/worker_events.rs
  • crates/broker/src/terminal_control.rs
  • packages/cli/src/cli/commands/local-agent.test.ts
  • packages/cli/src/cli/commands/local-agent.ts
  • packages/cli/src/cli/lib/attach-fleet-node.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • CHANGELOG.md
  • crates/broker/src/runtime/init.rs
  • crates/broker/src/terminal_control.rs
  • packages/cli/src/cli/commands/local-agent.ts
  • crates/broker/src/runtime/fleet.rs
  • packages/cli/src/cli/lib/attach-fleet-node.ts

Comment thread crates/broker/src/runtime/event_loop.rs
Comment thread packages/cli/src/cli/commands/local-agent.test.ts

@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 11 files (changes from recent commits).

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

Re-trigger cubic

Comment thread crates/broker/src/runtime/event_loop.rs
Comment thread crates/broker/src/runtime/maintenance.rs Outdated
Comment thread crates/broker/src/runtime/api.rs Outdated
Comment thread crates/broker/src/runtime/event_loop.rs
Comment thread packages/cli/src/cli/lib/attach-fleet-node.ts Outdated
Comment thread packages/cli/src/cli/commands/local-agent.test.ts

@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.

Caution

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

⚠️ Outside diff range comments (4)
packages/cli/src/cli/lib/attach-fleet-node.ts (4)

452-458: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fully stop remote transport during shutdown.

close() closes remote only when its state is OPEN. If shutdown occurs while the initial or resumed socket is CONNECTING, that socket can establish after the proxy has closed.

The reconnect timer is also not stored or cleared. Shutdown during backoff therefore keeps the timer active and can delay process exit. Store and clear the reconnect timer. Terminate any non-closed remote socket during close().

Also applies to: 466-480

🤖 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 `@packages/cli/src/cli/lib/attach-fleet-node.ts` around lines 452 - 458, Update
the reconnect logic in connect and close so the reconnect timeout is stored and
cleared during shutdown, preventing delayed reconnect attempts. Ensure close()
terminates remote whenever it is not already CLOSED, including CONNECTING
sockets, and prevent pending reconnect callbacks from initiating a new
connection after shutdown.

154-164: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make terminal readiness connection-scoped.

ready resolves only once. After a reconnect, /snapshot can return the old snapshot before the new WebSocket sends its terminal.ready frame. Existing /ws clients also do not receive the new ANSI snapshot because the terminal.ready handler only updates local state.

The resize route has the same gap. It checks only remote.readyState, but crates/broker/src/runtime/fleet.rs rejects resize requests until session.ready is true at Lines [275-281]. The proxy can therefore return HTTP 200 with applied: true while the broker rejects the resize.

Create readiness state per connection generation. Gate snapshot and resize requests on the current generation. Resynchronize existing event sockets when the new snapshot arrives.

Also applies to: 253-274, 394-410

🤖 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 `@packages/cli/src/cli/lib/attach-fleet-node.ts` around lines 154 - 164,
Replace the single-use readiness promise around ready, resolveReady,
rejectReady, and readySettled with readiness state tied to each WebSocket
connection generation. Update snapshot and resize handlers to await the current
generation’s terminal-ready state, and report resize success only after the
broker accepts the request. In the terminal.ready handling and existing /ws
event-socket flow, publish the newly received ANSI snapshot so reconnecting
clients are resynchronized instead of retaining stale data.

288-298: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Retain output when no event socket accepts it.

The code checks eventSockets.size before calling broadcast. If the set contains a closed or backpressured socket, broadcast sends to no client, but the current chunk is not added to outputHistory. The next /ws connection cannot replay that chunk.

Make broadcast report whether an event socket accepted the frame. Retain or explicitly report the chunk when no event socket accepts it.

Also applies to: 411-423

🤖 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 `@packages/cli/src/cli/lib/attach-fleet-node.ts` around lines 288 - 298, Update
broadcast in attach-fleet-node.ts to return whether at least one open,
non-backpressured event socket successfully accepts the encoded frame, while
preserving removal of unusable sockets. Use that result at both broadcast call
sites (including the later occurrence) to retain the current chunk in
outputHistory or explicitly report it when no socket accepts it, rather than
relying only on eventSockets.size.

437-443: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

End the local session when the broker sends terminal.closed.

This branch reports only to inputSockets. A client connected only to /ws receives no terminal-closed signal. The reconnect handler then treats the terminal closure as a transient transport failure and retries the resume URL.

Reject initial readiness when necessary. Notify or close eventSockets. Suppress reconnect attempts for a terminal closure. Use a terminal-specific error instead of eventually reporting node_unreachable.

🤖 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 `@packages/cli/src/cli/lib/attach-fleet-node.ts` around lines 437 - 443, Update
the terminal.closed branch in the attach-fleet-node session flow to terminate
the local session, not only broadcast pty_input_error through inputSockets.
Reject initial readiness when closure occurs, notify or close eventSockets for
/ws clients, and mark the closure as terminal-specific so the reconnect handler
suppresses resume attempts and does not report node_unreachable.
🤖 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.

Outside diff comments:
In `@packages/cli/src/cli/lib/attach-fleet-node.ts`:
- Around line 452-458: Update the reconnect logic in connect and close so the
reconnect timeout is stored and cleared during shutdown, preventing delayed
reconnect attempts. Ensure close() terminates remote whenever it is not already
CLOSED, including CONNECTING sockets, and prevent pending reconnect callbacks
from initiating a new connection after shutdown.
- Around line 154-164: Replace the single-use readiness promise around ready,
resolveReady, rejectReady, and readySettled with readiness state tied to each
WebSocket connection generation. Update snapshot and resize handlers to await
the current generation’s terminal-ready state, and report resize success only
after the broker accepts the request. In the terminal.ready handling and
existing /ws event-socket flow, publish the newly received ANSI snapshot so
reconnecting clients are resynchronized instead of retaining stale data.
- Around line 288-298: Update broadcast in attach-fleet-node.ts to return
whether at least one open, non-backpressured event socket successfully accepts
the encoded frame, while preserving removal of unusable sockets. Use that result
at both broadcast call sites (including the later occurrence) to retain the
current chunk in outputHistory or explicitly report it when no socket accepts
it, rather than relying only on eventSockets.size.
- Around line 437-443: Update the terminal.closed branch in the
attach-fleet-node session flow to terminate the local session, not only
broadcast pty_input_error through inputSockets. Reject initial readiness when
closure occurs, notify or close eventSockets for /ws clients, and mark the
closure as terminal-specific so the reconnect handler suppresses resume attempts
and does not report node_unreachable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d906a686-2069-44b7-a729-b1952564089b

📥 Commits

Reviewing files that changed from the base of the PR and between 4771be7 and cdc36d7.

📒 Files selected for processing (5)
  • crates/broker/src/runtime/event_loop.rs
  • crates/broker/src/runtime/fleet.rs
  • crates/broker/src/runtime/worker_events.rs
  • packages/cli/src/cli/commands/local-agent.ts
  • packages/cli/src/cli/lib/attach-fleet-node.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/broker/src/runtime/worker_events.rs
  • crates/broker/src/runtime/event_loop.rs
  • packages/cli/src/cli/commands/local-agent.ts

@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 5 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/cli/src/cli/commands/local-agent.ts
Comment thread packages/cli/src/cli/commands/local-agent.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.

All reported issues were addressed across 2 files (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/commands/local-agent.ts

@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.

2 issues found across 10 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="crates/broker/src/runtime/fleet.rs">

<violation number="1" location="crates/broker/src/runtime/fleet.rs:160">
P1: A terminal write timeout can corrupt the shared worker command stream instead of failing only this attach: cancellation may leave a partial JSON frame, and the next worker command is then consumed as an invalid line. Avoid cancelling a partially written framed stdin operation—serialize it behind a dedicated writer and reset/terminate the worker when a write cannot complete.</violation>

<violation number="2" location="crates/broker/src/runtime/fleet.rs:263">
P2: Near the terminal queue reserve, a failed snapshot/input/resize reports two `terminal.closed` frames: `send_terminal` queues an `output_backpressure` close when the error is rejected, then `fail_terminal_session` queues another close with the real code. Make failure reporting emit at most one close and preserve the original failure reason.</violation>
</file>

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

Re-trigger cubic

// from worker_events to the terminal lane, so this never
// stalls heartbeat/action processing behind a PTY snapshot.
let request_id = format!("terminal_snapshot_{}", Uuid::new_v4().simple());
match tokio::time::timeout(

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.

P1: A terminal write timeout can corrupt the shared worker command stream instead of failing only this attach: cancellation may leave a partial JSON frame, and the next worker command is then consumed as an invalid line. Avoid cancelling a partially written framed stdin operation—serialize it behind a dedicated writer and reset/terminate the worker when a write cannot complete.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/broker/src/runtime/fleet.rs, line 160:

<comment>A terminal write timeout can corrupt the shared worker command stream instead of failing only this attach: cancellation may leave a partial JSON frame, and the next worker command is then consumed as an invalid line. Avoid cancelling a partially written framed stdin operation—serialize it behind a dedicated writer and reset/terminate the worker when a write cannot complete.</comment>

<file context>
@@ -135,17 +157,18 @@ impl BrokerRuntime {
-                        match self
-                            .workers
-                            .send_to_worker(
+                        match tokio::time::timeout(
+                            TERMINAL_WORKER_WRITE_TIMEOUT,
+                            self.workers.send_to_worker(
</file context>

.count()
>= TERMINAL_INPUT_MAX_IN_FLIGHT_PER_SESSION
{
self.send_terminal(TerminalToCloud::Error {

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.

P2: Near the terminal queue reserve, a failed snapshot/input/resize reports two terminal.closed frames: send_terminal queues an output_backpressure close when the error is rejected, then fail_terminal_session queues another close with the real code. Make failure reporting emit at most one close and preserve the original failure reason.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/broker/src/runtime/fleet.rs, line 263:

<comment>Near the terminal queue reserve, a failed snapshot/input/resize reports two `terminal.closed` frames: `send_terminal` queues an `output_backpressure` close when the error is rejected, then `fail_terminal_session` queues another close with the real code. Make failure reporting emit at most one close and preserve the original failure reason.</comment>

<file context>
@@ -224,18 +253,33 @@ impl BrokerRuntime {
+                    .count()
+                    >= TERMINAL_INPUT_MAX_IN_FLIGHT_PER_SESSION
+                {
+                    self.send_terminal(TerminalToCloud::Error {
+                        session_id,
+                        code: "input_backpressure".into(),
</file context>

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.

No way to attach to an agent running on another fleet node

3 participants