feat(remote): client-side /acp liveness — heartbeat, resume, turn recovery - #55
Merged
Conversation
…overy
The Studio `/acp` client had no proactive liveness: an idle socket sat silent
until an intermediary RST it mid-think ("Connection reset without closing
handshake"), each reconnect opened a *fresh* `session/new` (stacking server-side
sessions), and an in-flight turn was dropped with no trace. This wires four
client-side guards, all surfaced as `app-log` lines in the Activity panel.
1. Idle heartbeat. A `tokio::time::interval` arm probes an idle socket with a
request the gateway answers itself (`studio/ping` -> `-32601`): no agent turn,
no tokens, answered even mid-prompt. Any inbound frame counts as liveness; a
probe unanswered for a full interval => half-open => reconnect. Keeps the
socket warm too, so the idle-RST churn stops at its source.
2. session/resume on reconnect. The `Session` now persists across attempts
(owned by `run_reconnecting`); a reconnect resumes the existing agent session
instead of stacking a new one. If the gateway has already reaped it, the
resume error falls back to `session/new`.
3. Abandoned-turn notice. When the socket ends with a turn in flight, record it
in Activity (the panel already closes the spinner off `remote-status`).
4. Turn ceiling. A `session/prompt` with no result past PROMPT_TIMEOUT (600s)
invalidates the socket and reconnects rather than trusting a wedged peer
(ADR browser-tunnel-liveness R3).
acp-tunnel gains `Session::heartbeat()` and `Session::forget_session()` (+ tests,
25 pass). Note: this is the *client* half — it stops Studio triggering/enduring
the churn, but the seat-burning server-side zombie (orphaned turn on a half-open
socket) still needs the gateway-side liveness + cancel-on-teardown in openab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erity
Folds the Activity-log audit into this PR. Two problems:
- The core writes ALL its logs to stderr (stdout is the JSON-RPC channel), but
mcp.rs emitted every stderr line at `warn` — so benign INFO lines ("Scaling…",
"✓ Desired count set…") rendered orange. Now `core_level()` levels off the
line's own text and defaults benign lines to `info`.
- Three prefix styles coexisted (`subsystem:`, `[core]`, none) with mixed
capitalization. Normalized every Activity line to `<subsystem>: <lowercase>`
over eight subsystems (app / core / remote / roster / chat / identity / config
/ update); `…` (U+2026) for in-progress, no trailing punct for terminal.
Also adds three lines that were missing from the reconnect story:
- `remote: dialing <url>…` (the URL carries no secret — bearer is header-only)
- `remote: reconnecting in 5s…` (explains the gap before the next attempt)
- `remote: disconnected by user`
Touches src-tauri/{mcp,lib,remote}.rs and console/src/{main,log}.ts. Console
build + 59 tests pass; Rust validated on the macOS `desktop` job.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # src-tauri/src/remote.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wires the four Studio-side fixes from the
/acpzombie/RST investigation. All four surface asapp-loglines so they're readable in the Activity panel.Context
Symptoms seen in the field: repeated
remote: ws read: WebSocket protocol error: Connection reset without closing handshakeevery few minutes, and an Orca session that only cleared after bouncing the ECS task. Root cause is a silent idle socket with no client-side liveness. This PR is the client half.What changed
src-tauri/src/remote.rs—run_reconnectingnow owns a persistentSession;run_oncegains a liveness timer and threads its exit reason out through anoutcomeso every exit path runs cleanup.tokio::time::intervalarm probes an idle socket withstudio/ping— a method the gateway answers itself with-32601(no agent turn, no tokens, answered even mid-prompt). Any inbound frame = liveness; a probe unanswered for a full interval ⇒ half-open ⇒ reconnect. Also keeps the socket warm, so the idle-RST churn stops at its source.session/resumeon reconnect (item 2). TheSessionpersists across attempts; a reconnect resumes the existing agent session instead of stacking a freshsession/neweach time. If the gateway already reaped it, the resume error falls back tosession/new.remote-status, so this is the observability half).session/promptwith no result pastPROMPT_TIMEOUT(600s) invalidates the socket and reconnects rather than trusting a wedged peer — ADR browser-tunnel-liveness R3.crates/acp-tunnel/src/lib.rs— addsSession::heartbeat()(the probe builder) andSession::forget_session()(resume→new fallback), each with a unit test.New Activity log lines
remote: reconnecting — will resume the existing session(info)remote: session resumed — oab tools republished(info)remote: session/resume rejected (…) — opening a fresh session(warn)remote: liveness probe unanswered — socket half-open, reconnecting(warn)remote: turn exceeded 600s with no result — dropping the socket and reconnecting(warn)remote: connection dropped with a turn in flight — turn abandoned(warn)Tests
cargo test -p acp-tunnel→ 25 passed.studio-desktopcompiles on the macOSdesktopjob (the Linux compile-gate is disabled and this box lacks the GTK/WebKit stack);remote.rswas rustc-parse-checked here (no syntax/type errors in reach).Not in this PR (server side — needs your go)
This is the client half. The seat-burning zombie — an orphaned agent turn on a half-open socket the gateway can't detect — still needs, in
openabdev/openabcrates/openab-gateway: (a) server-initiated liveness (WS ping / read-idle timeout so the read loop actually breaks), and (b) firing each session'scancelNotifyon teardown, not justabort()ing the relay task. I'll open that as a separate openab issue for review before touching it.🤖 Generated with Claude Code