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):
- Hash the identity bytes with FNV-1a 64.
first = hash % 128,second = (hash / 128) % 128.- If both indexes are equal,
second = (second + 1) % 128, soQueer QueerbecomesQueer 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) andpreferences.remchild.allowNonLan. - The launch argument
--allow-non-lan(orVESKSLOP_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.