Files
Veskslop-t/remchild/plan.md
T

6.1 KiB

remchild — remote child plan

remchild is a separate module that runs on a remote machine. Veskslop feeds it work and it feeds results back. Planned uses: STT (local SAM/VOX stack or an AI model), TTS, later video (a window or an MPV file), and finally the full desktop control app.

Status: PoC on branch remchild (client + server + naming). No Web UI yet.

What the PoC does

Piece Path Notes
Client client/main.c C11, libc only, no runtime dependencies. Linux.
Word lists words.json, client/words.h Two lists of 128 words. The header is generated for the client.
Server server/server.mjs Node, no dependencies. Proves the listener, allowlist, heartbeat, ping test, and history.
Test client/remchild --once One hello, one ping, one pong, exit.

Build and run:

cc -O2 -o remchild/client/remchild remchild/client/main.c
node remchild/server/server.mjs --port 8799 --ips 192.168.1.5
./remchild/client/remchild --server 192.168.1.5:8799

The client prints its assigned name, connects, and shows p (ping test) and q (quit). The server prints each connect, state change, and disconnect, and appends to server/clients.jsonl.

Naming

Each client gets two words. The pair comes from the first non-loopback MAC address (fallback /etc/machine-id):

  1. Hash the identity bytes with FNV-1a 64.
  2. first = hash % 128, second = (hash / 128) % 128.
  3. If both indexes are equal, second = (second + 1) % 128, so Queer Queer becomes Queer Groin.

The lists share words on purpose, so Funky Monkey, Funky Queer, and Queer Monkey are all possible. A random MAC changes the name; a stable machine-id does not.

Heartbeats and states

The client sends heartbeat on this schedule after boot:

Time since start Interval
0 to 60 s 10 s
60 s to 6 min 30 s
after 6 min 5 min

The server keeps three states from lastSeen:

If the server misses a heartbeat during the fast phases (10 s or 30 s; roughly the first six minutes), it sends the client a notice with the miss count. The client prints it as a warning. Nothing else changes: it is information for the user, not a reconnect trigger. The steady 5 minute phase relies on the stale and gone states instead.

State Age Effect
active under 12 min Jobs and commands may be sent
stale 12 to 30 min No jobs or commands
gone over 30 min Considered offline; a late heartbeat revives it

First and last seen are kept in memory and written to clients.jsonl. SIGINT prints the table.

Discovery

The server answers probes and beacons on UDP 8798 (--discovery-port, --beacon-ms, 0 disables the beacon). Probes go to the subnet broadcast and to loopback; the server replies unicast, so the client reads the address from the reply. Beacons carry {type, name, port, id} and go to every interface's broadcast address, for passive listeners.

remchild with no --server spends five seconds probing. One server connects automatically, several show a pick list, none falls back to the typed host:port prompt. --server always wins and skips discovery. Discovery only crosses one subnet: WiFi client isolation, VLANs, and VPNs block it, and the Web UI cannot use it at all.

Transport

Newline-delimited JSON over TCP on a dedicated port (PoC: 8799), plus the UDP discovery above. No Discord connection, no HTTP framework.

Direction Message Meaning
client → server hello id, name, platform, version
server → client welcome Assigned name, heartbeat interval, server name
client → server heartbeat ts
both ping / pong seq echo, for the test button and latency
client → server probe UDP discovery ask (veskslop-remchild-probe)
server → client beacon UDP discovery answer and broadcast (veskslop-remchild)
server → client notice Information, e.g. heartbeat-missed with a count
server → client job Later: a unit of work with an ID and a payload
client → server job-result Later: id, ok, data or error

Binary payloads (PCM, video frames) get their own framing later: a length-prefixed binary frame on the same socket, or a second UDP channel for audio where loss is cheap.

Server side in Veskslop

The PoC server moves into the daemon as poc/src/remchild-server.ts:

  • Separate listener, default port 8799 (VESKSLOP_REMCHILD_PORT).
  • Settings: preferences.remchild.allow (IP list) and preferences.remchild.allowNonLan.
  • The launch argument --allow-non-lan (or VESKSLOP_REMCHILD_ALLOW_NON_LAN=1) is required before a non-LAN address in the list is accepted. Without the list, only LAN and Tailscale ranges pass.
  • The daemon never scans for clients. It listens, records, and dispatches.
  • The client registry feeds a Status section later; no Web UI work is needed now.

Phases

Phase Work
M1 (done) PoC: naming, listen, allowlist, heartbeat, ping test, history, UDP discovery, heartbeat-miss notices
M2 Move the server into the daemon, add settings, the launch-arg unlock, and tests. Add job dispatch and job-result with retry and dedupe.
M3 Audio: the daemon sends call audio to the client, the client runs VAD and STT (SAM/VOX or an AI model), and returns annotated text and voice commands. The same path carries TTS back.
M4 Video: stream a window or play a file with MPV, and return frames or a stream URL.
M5 Desktop: an ImGui control app. Wayland first: pick a Wayland-capable backend (SDL3, or SDL2/GLFW built with Wayland and libwayland-dev, wayland-protocols, xkbcommon) or a native wlroots/EGL backend. X11 is a fallback only. The terminal UI in the PoC is a placeholder because the current host has no GUI dev libraries.

Open questions

  • Track-to-user audio mapping for remote STT (see ../docs/voice-audio-split.md).
  • Job retry policy and idempotency keys.
  • Whether audio uses the same socket or a second channel.
  • Wake-on-LAN or reconnect backoff for a client that drops.