Overview
The Rubber Duck CLI operates through a persistent daemon process that manages Pi RPC sessions and coordinates between the terminal client, voice app, and AI backend.
Terminal CLI ──┐
├──► Daemon ──► Pi Process (per session)
Voice App ─────┘ │
├──► MetadataStore
└──► EventBus
Auto-Start Behavior
The daemon starts automatically when you run any duck command:
- Socket check: CLI attempts to connect to the Unix socket
- PID validation: If socket missing, checks PID file for running daemon
- Spawn: If not running, spawns detached daemon process
- Polling: Waits up to 5 seconds with exponential backoff (100ms, 200ms, 400ms…)
- Connect: Once socket is ready, command proceeds
You never need to manually start the daemon.
Socket Location
The daemon communicates via Unix domain socket.
Default Path
~/Library/Application Support/RubberDuck/daemon.sock
Fallback Path
If the default path exceeds Unix socket length limits (~104 characters), the daemon and CLI automatically fall back to:
$TMPDIR/rubber-duck-<hash>.sock
The hash is derived from the app support directory path to ensure consistency across invocations.
Finding the Socket
You can programmatically resolve the socket path:
import { SOCKET_PATH } from "rubber-duck";
console.log(SOCKET_PATH);
Or check the PID file:
cat ~/Library/Application\ Support/RubberDuck/duck-daemon.pid
Runtime Files
All daemon state lives under:
~/Library/Application Support/RubberDuck/
File Structure
Workspace and session state. Contains workspace paths, session IDs, names, and last active timestamps. Written atomically via temp file + rename.
Daemon configuration. Model selection, thinking level, and Pi binary path.
Daemon lifecycle log. Startup, shutdown, errors, and health check events. Appended on each daemon invocation.
Process ID of the running daemon. Used to detect stale sockets and prevent duplicate daemon processes.
Unix domain socket for IPC. NDJSON protocol for requests, responses, and events.
Per-session Pi conversation files. Each session has its own .pi file containing the full conversation history.
Example Layout
~/Library/Application Support/RubberDuck/
├── metadata.json
├── config.json
├── duck-daemon.log
├── duck-daemon.pid
├── daemon.sock
└── pi-sessions/
├── session-abc123.pi
└── session-xyz789.pi
IPC Protocol
The daemon uses NDJSON (newline-delimited JSON) over the Unix socket.
{"id":"req-123","method":"say","params":{"message":"hello"}}
Unique request ID for correlation (UUID or sequential)
Daemon method name (see Methods below)
Method-specific parameters
{"id":"req-123","ok":true,"data":{"sessionId":"session-abc"}}
Success indicator (true/false)
Response payload (when ok=true)
Error message (when ok=false)
Events are pushed to subscribed clients (no request ID):
{"event":"pi","sessionId":"session-abc","data":{"type":"message_update","assistantMessageEvent":{"type":"text_delta","text":"Hello"}}}
Event type: pi (Pi protocol event) or app_history (voice history)
Session that emitted the event
Event payload (PiEvent for pi events)
Daemon Methods
Session Management
attach - Create or resume a workspace session
{"method":"attach","params":{"path":"/Users/alice/projects/app"}}
follow - Subscribe to session events
{"method":"follow","params":{"sessionId":"session-abc","skipHistory":false}}
unfollow - Unsubscribe from session events
{"method":"unfollow","params":{"sessionId":"session-abc"}}
sessions - List all sessions
{"method":"sessions","params":{"all":true}}
say - Send message to session
{"method":"say","params":{"message":"run tests","sessionId":"session-abc"}}
abort - Cancel running agent turn
{"method":"abort","params":{"sessionId":"session-abc"}}
Voice App Integration
voice_connect - Register voice app client
{"method":"voice_connect","params":{}}
voice_tool_call - Execute tool from voice app
{"method":"voice_tool_call","params":{"name":"read_file","arguments":{"path":"/src/app.ts"},"callId":"call-123","sessionId":"session-abc"}}
voice_state - Get current voice session state
{"method":"voice_state","params":{}}
Diagnostics
ping - Health check
{"method":"ping","params":{}}
Response: {"ok":true,"data":{"pong":true}}
doctor - Run health checks
{"method":"doctor","params":{}}
Returns daemon-side health check results.
get_state - Get session state
{"method":"get_state","params":{"sessionId":"session-abc"}}
UI Extension
extension_ui_response - Respond to interactive prompt
{"method":"extension_ui_response","params":{"id":"ui-req-123","response":{"confirmed":true}}}
Process Management
Daemon Process
The daemon runs as a detached background process:
# Check if daemon is running
ps aux | grep duck-daemon
# View daemon logs
tail -f ~/Library/Application\ Support/RubberDuck/duck-daemon.log
# Kill daemon manually (will auto-restart on next `duck` command)
kill $(cat ~/Library/Application\ Support/RubberDuck/duck-daemon.pid)
Pi Subprocesses
The daemon spawns one Pi RPC subprocess per active session:
# List Pi processes
ps aux | grep "pi --mode rpc"
Each Pi process:
- Runs
pi --mode rpc --model <model>
- Communicates via stdin/stdout NDJSON
- Maintains a single session conversation
- Is killed when the session ends or daemon shuts down
Health Monitoring
The daemon performs periodic health checks every 30 seconds:
- Pi liveness: Verifies each Pi subprocess is responsive
- Socket cleanup: Removes disconnected clients
- Session pruning: Removes inactive sessions (configurable)
Unresponsive Pi processes are automatically restarted.
Running the Daemon Manually
For debugging, you can run the daemon in the foreground:
# From source
cd cli
node dist/daemon.js --verbose
# Via installed binary (symlink or --daemon flag)
duck-daemon --verbose
# OR
duck --daemon --verbose
Foreground mode:
- Logs to stdout instead of
duck-daemon.log
--verbose flag shows all Pi events
- Ctrl+C to stop
Single-Binary Mode
The duck binary acts as both CLI and daemon depending on how it’s invoked:
CLI mode (default):
Daemon mode (via symlink or flag):
duck-daemon # Symlink triggers daemon mode
duck --daemon # Flag triggers daemon mode
The macOS app installer creates a duck-daemon symlink so the Swift app can launch the daemon using the same binary.
Programmatic Usage
Connect to the daemon from Node.js:
import { DaemonClient } from "rubber-duck";
const client = await DaemonClient.connect();
// Send a request
const response = await client.request("attach", {
path: "/Users/alice/project",
});
if (response.ok) {
const { session } = response.data;
console.log("Session:", session.id);
}
// Listen for events
client.onEvent((event) => {
if (event.event === "pi") {
console.log("Pi event:", event.data);
}
});
// Clean up
client.close();
Troubleshooting
Daemon won’t start
# Check for stale PID file
cat ~/Library/Application\ Support/RubberDuck/duck-daemon.pid
ps -p <pid> # If process doesn't exist, remove PID file
# Check socket permissions
ls -la ~/Library/Application\ Support/RubberDuck/daemon.sock
# Remove stale socket
rm ~/Library/Application\ Support/RubberDuck/daemon.sock
# Try again
duck doctor
Socket connection timeout
If the socket path is too long, ensure the fallback path is writable:
echo $TMPDIR
ls -la $TMPDIR/rubber-duck-*.sock
High memory usage
Each Pi subprocess can consume significant memory. Check active sessions:
Kill idle sessions by stopping the daemon:
kill $(cat ~/Library/Application\ Support/RubberDuck/duck-daemon.pid)
Logs missing events
Daemon log shows lifecycle events only. For Pi event logs, run in foreground: