Skip to content

Low-latency command delivery — ride the existing tunnel control channel instead of waiting for the next heartbeat #23

Description

@xerhab

Context

Session commands are queued hub-side (queueCommand() in agent-hub/server.js) and only delivered on the next heartbeat reply — worst case HUB_INTERVAL = 20s after the click, plus the beat the agent then fires immediately after executing (the extra-beat logic in run_forever()). The UI papers over the gap with optimistic pending spinners.

Meanwhile, tunnel-agent.js already holds a persistent WebSocket control channel to the hub that today carries exactly one message type ({open, port}) and exists on every host.

Problem

Every session action (spawn/kill/start/restart/delete — and more with the input/push/PR features) feels sluggish: click → up to 20s of spinner → execution → next beat before state reflects. Raising the heartbeat rate is the wrong fix (usage parsing, git calls, and payload size scale with it).

Proposal

Use the control channel as a wake-up signal, keeping the heartbeat as the single source of truth for state and command delivery:

  • Hub: when a command is queued for host X and controlChannels[X] is live, send {poke: true} on it.
  • Agent: tunnel-agent.js writes a byte to a tiny local notification mechanism shared with the manager — simplest is a named pipe or a loopback UDP datagram that hub-agent.py selects on with a INTERVAL-second timeout instead of time.sleep(INTERVAL). On poke: beat immediately, receive the queued command in the reply, execute, beat again (existing logic).
  • Result: command latency drops from ~0–20s to sub-second on hosts with a live tunnel, with zero change to the delivery semantics (still at-least-once via the heartbeat queue + cmdId de-dup; hosts with a broken tunnel degrade to today's behavior).
  • The select-loop change also fixes a small existing wart: the manager currently can't react to anything mid-sleep.

Alternatives considered

  • Commands in the control channel: faster still, but creates a second delivery path with its own ordering/de-dup/auth concerns — the poke design keeps one authoritative path.
  • Hub-side long-poll on /api/heartbeat: holds sockets open through Cloudflare; the tunnel WS already solved that problem once.

Touchpoints

  • agent-hub/server.js — poke on queue; controlChannels[name].sendOpen gains a sibling
  • agent/tunnel-agent.js — handle {poke}, local relay
  • agent/hub-agent.py — interruptible wait in run_forever()

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions