Add the remchild PoC: client, server, naming, and plan
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user