Add the remchild PoC: client, server, naming, and plan

This commit is contained in:
Fisto
2026-09-22 09:34:43 +02:00
parent 10143c87a4
commit 9a776fefd3
7 changed files with 1228 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
# 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:
```sh
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`:
| 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.
## Transport
Newline-delimited JSON over TCP on a dedicated port (PoC: 8799). 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 |
| 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 |
| 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: a rust/ImGui control app (Dear ImGui over GLFW or SDL2). 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](../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.