The Services That Make Mate Engine Mac X Talk
The app is only the front door. Behind its handshake and command features sits a chain of small, independent services — each with its own life, its own failure modes, and its own story.
A note on devices. What follows is the arrangement we run, built from the machines we happened to have — a Windows box for the network hop, the app on a Mac, and the TTS server on a separate Mac mini. None of that is a requirement. Every service here is device-agnostic: you can spread them across whatever mix of machines you own, or collapse all of them onto a single machine — including one Mac, provided it is strong enough to host the app and the speech model together. The chain is what matters; the hardware is not. Read the inventory below as one way to do it, not the way.
Why this article exists
Mate Engine Mac X runs on the Mac. But the features that make the familiar feel alive — knowing when it is online, receiving commands, and speaking in character — are not implemented inside the app. They are supplied by a set of downstream services we stood up alongside it.
This is a living inventory. It starts as an outline of the services that exist today, and it will grow as a log of the problems we hit and how we addressed them.
The connectivity chain
MateEngineX (Mac app)
|
v
Windows portproxy (netsh) <- static rule on <WINDOWS-HOST>
|
v
WSL2 socat relay <- container30051.service inside WSL2
|
v
container bridge <CONTAINER-IP>
|
v
handshake daemon :30051 <- the spoke that answers the app
Speech travels a separate, out-of-band path:
handshake daemon :30051
| (say.py enqueues a cue)
v
MateEngineX polls -> receives cue
|
v
GPT-SoVITS TTS <MAC-MINI-LAN-IP>:9880 <- voices the line in character
The service inventory
1. MateEngineX (the Mac app)
The familiar itself. It owns the character, the window, and the voice rendering. It holds two settings that matter to everything below: daemonUrl (where it polls for commands) and ttsApiUrl (where it sends text to be spoken). It polls the daemon on a short interval — that poll is its heartbeat; a handshake is not.
2. Windows portproxy (netsh)
The first hop off the Mac. A static netsh portproxy rule on <WINDOWS-HOST> forwards TCP 30051 from the LAN to the WSL2 side. Being static, it survives reboots on its own — it is rarely the thing that breaks, but it is the thing that must be reachable for anything else to work.
3. WSL2 socat relay
The middle hop, and historically the fragile one. A socat TCP-LISTEN:30051,fork,reuseaddr TCP:<CONTAINER-IP>:30051 process living inside WSL2, kept up by the systemd unit container30051.service. It is what actually carries traffic from the Windows portproxy into the container bridge. It lives outside the container, so nothing inside can heal it — when it dies, the whole chain goes dark.
4. Handshake daemon (:30051)
The spoke that answers. A small Python daemon inside the container that listens on :30051, records handshakes and polls, and holds a queue of pending speak commands for the app to collect. Its state lives in handshakes.jsonl and commands.jsonl.
5. say.py (command CLI)
The way we talk to the pet programmatically: enqueue a line of text as a speak command. Cues are delivered on the next poll and acknowledged by the app.
6. notifyd (:30052)
The notification router. It sits beside the handshake daemon and decides which channel a given notification type should reach. It is not in the app's direct path, but it is part of the same small neighbourhood of services.
7. GPT-SoVITS TTS (<MAC-MINI-LAN-IP>:9880)
The voice. A GPT-SoVITS server that renders the pet's speech. The app POSTs the cue text to ttsApiUrl verbatim; the server runs in a response/role-play mode that improvises a sentence-length reply in character rather than reading the text literally. It does not have to run on the same box as the app — and currently does not.
8. catherder (supervisor)
The supervisor for the in-container services. It keeps the daemon and friends alive. Deliberately out of scope: the WSL2 socat relay, which is external and supervised by WSL2 itself.
How a command reaches the pet
- Something enqueues a cue via
say.py. - The app polls the daemon every couple of seconds.
- The daemon delivers any pending cues; the app acknowledges them.
- The app posts the text to the TTS endpoint, and the voice improvises the line in character.
Cues are real-time only. If the client was absent, stale cues are dropped rather than saved for later — a gap in connectivity is a gap in speech.
The update log (living section)
This is where discovered issues and their fixes get appended over time.
2026-09-05 — the socat relay silently died
The relay was found missing from the registry after it silently died on a WSL2 VM restart, breaking the entire chain: the app could not handshake, poll, or acknowledge. It was added to the daemon registry and given a systemd unit (container30051.service) so it could auto-restart. The lesson: the middle hop is external, and nothing inside the container can recover it.
2026-09-25 — GPT-SoVITS moved off the Mac
The TTS server was moved from the Mac's own loopback (127.0.0.1:9880) onto a separate Mac mini (<MAC-MINI-LAN-IP>:9880), under a LaunchAgent. The install serves its API at the bare root, so the app's ttsApiUrl must be the root URL with no /tts path — it POSTs text and parameters verbatim, and the shipped defaults bake in a path that this install does not use. The voice stopped being tied to the machine running the app.
2026-10-01 — the relay died again; a startup task to self-heal
After a container/WSL2 restart the relay silently died once more. The external probe reset (connection refused/reset), the app went offline for roughly 19.5 hours, and six queued speech cues were lost — real-time-only delivery means a backlog is not a buffer, it is a gap. The fix was the same manual one-liner: sudo systemctl enable --now container30051 inside WSL2. To stop repeating it, we drafted a Windows startup task (SuperClaw-WSL-Relay-Keepalive) that launches the distro, enables and starts the unit, probes the relay, and reports — so the chain self-heals after a reboot instead of waiting for an operator.
What is next
- Wire the startup task on <WINDOWS-HOST> and confirm it survives a reboot.
- Keep this log current: every future outage or fix gets its own dated entry above.