Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src-tauri/src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ async fn run_once<R: Runtime>(
// single writer stays in the loop, so frames are still serialized on the wire.
let (reply_tx, mut reply_rx) = mpsc::unbounded_channel::<Value>();

// Keepalive: Cloudflare's tunnel idle-closes a WS with no traffic (~100s) and
// tokio-tungstenite never pings on its own, so an idle `/acp` connection flaps
// roughly every 2 min — and until `session/resume` lands, each reconnect opens
// a fresh channel (lost agent context). A periodic WS Ping well inside that
// window counts as traffic and keeps the tunnel open between prompts. `Skip`
// missed-tick behaviour avoids a burst of pings if the loop was ever busy.
let mut keepalive = tokio::time::interval(std::time::Duration::from_secs(45));
keepalive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

// Loop over BOTH inbound frames and outbound chat actions. `write` never leaves
// the task; commands reach it only through `out_rx`.
loop {
Expand Down Expand Up @@ -280,6 +289,23 @@ async fn run_once<R: Runtime>(
send(&mut write, &reply).await?;
}

// Keepalive tick: send a WS Ping directly on `write` (the `send` helper
// only frames JSON Text). The read arm ignores the returning Pong
// (`Ping | Pong | Frame => continue`), so this composes with the rest of
// the loop without touching the inbound path.
_ = keepalive.tick() => {
write
.send(WsMessage::Ping(Vec::new()))
.await
.map_err(|e| format!("ws keepalive ping: {e}"))?;
// Surface each keepalive in the Activity pane so the operator can
// see the tunnel being kept warm between prompts.
let _ = app.emit(
"app-log",
json!({ "level": "info", "msg": "remote: keepalive ping sent" }),
);
}

// Inbound: a frame from the gateway.
msg = read.next() => {
let Some(msg) = msg else { break };
Expand Down
Loading