How the No-Mistakes IPC Socket Enables Daemon-Client Communication

The no-mistakes CLI and daemon communicate via a lightweight JSON-RPC protocol over a Unix-domain socket located at <NM_HOME>/daemon.sock, supporting synchronous RPC calls and streaming event subscriptions.

The no-mistakes project separates its user-facing CLI from a background daemon to manage long-running operations efficiently. Understanding how the IPC socket for daemon-client communication functions is critical for troubleshooting connectivity issues or extending the tool's capabilities. The implementation relies on a minimal JSON-RPC 2.0 protocol transmitted over Unix-domain sockets (or Windows named pipes), with clear separation between server and client logic in the internal/ipc/ package.

IPC Server Architecture

The daemon-side implementation resides in internal/ipc/server.go, where a Server object manages socket lifecycle and request dispatching.

Server Initialization and Socket Binding

The server is instantiated via NewServer, which initializes maps for method handlers (handlers) and streaming handlers (streamHandlers). The Serve(socketPath) function opens the socket using an internal listen(socketPath) helper and enters an accept loop:

// Conceptual flow from internal/ipc/server.go
server := ipc.NewServer()
err := server.Serve("/path/to/daemon.sock")

This loop calls ln.Accept() for each incoming connection, spawning a new goroutine to handle the request via handleConn.

Connection Handling and Request Dispatch

Inside handleConn, the server reads line-delimited JSON messages from the connection. Each message is unmarshaled into a Request struct, then routed to the appropriate handler through the dispatch method. This architecture allows the daemon to register specific methods (such as MethodHealth or MethodRun) in internal/daemon/manager.go while keeping the transport layer generic.

Streaming Event Support

For streaming operations like MethodSubscribe, the server distinguishes between one-shot and persistent connections. When dispatch identifies a streaming method, it first sends an OK response to acknowledge the subscription, then hands the raw connection to the registered streaming handler. This handler can push a series of events back to the client until the connection closes or the client cancels.

Graceful Shutdown

The server supports clean termination through its Close method. When invoked, it closes the listener, signals a done channel, and waits for all in-flight connections to finish. The handleConn function monitors this done channel to abort ongoing requests when the daemon is stopping, preventing resource leaks.

IPC Client Implementation

The client-side logic in internal/ipc/client.go provides a Go API for connecting to the daemon and invoking remote procedures.

Dialing with Timeout Protection

Clients initiate connections using Dial(socketPath), which internally calls dialEndpoint. This function applies a configurable connectTimeout—controlled by the NM_DAEMON_CONNECT_TIMEOUT environment variable—to bound the net.Dial operation. If the timeout expires, the function returns a ConnectTimeoutError detectable via IsConnectTimeout:

client, err := ipc.Dial("/var/run/no-mistakes/daemon.sock")
if err != nil {
    if ipc.IsConnectTimeout(err) {
        log.Fatal("daemon is not responding")
    }
    log.Fatalf("connection failed: %v", err)
}
defer client.Close()

Synchronous RPC Calls

The client exposes Call and CallWithTimeout for synchronous operations. These methods serialize requests onto the underlying connection using a mutex (c.mu) to guarantee strict request-response ordering. Each call sets a read deadline of 30 seconds (defaultCallTimeout) on the socket to prevent indefinite hangs:

var health struct{ OK bool }
err := client.Call(ipc.MethodHealth, nil, &health)

Event Subscriptions

Long-running event streams are handled through the Subscribe function. This opens a dedicated connection to the socket, transmits a MethodSubscribe request with parameters (such as SubscribeParams containing a RunID), and returns a Go channel that receives Event structs. The function also returns a cancel function that closes the socket when the caller is done:

params := &ipc.SubscribeParams{RunID: "run-123"}
events, cancel, err := ipc.Subscribe(sock, params)
if err != nil {
    log.Fatal(err)
}
defer cancel()

for ev := range events {
    fmt.Printf("event: %s\n", ev.Type)
}

JSON-RPC Protocol Structure

Both client and server use message types defined in internal/ipc/protocol.go. The protocol follows JSON-RPC 2.0 conventions with minimal extensions:

type Request struct {
    JSONRPC string          `json:"jsonrpc"` // always "2.0"
    ID      int64           `json:"id"`
    Method  string          `json:"method"`
    Params  json.RawMessage `json:"params,omitempty"`
}

type Response struct {
    JSONRPC string          `json:"jsonrpc"` // always "2.0"
    ID      int64           `json:"id"`
    Result  json.RawMessage `json:"result,omitempty"`
    Error   *RPCError       `json:"error,omitempty"`
}

The Method field corresponds to handlers registered in internal/daemon/manager.go, including MethodHealth, MethodRun, and MethodSubscribe.

Lifecycle Management and Error Handling

The IPC socket for daemon-client communication implements several safeguards against dead connections and resource exhaustion:

  • Connect timeouts: The dialEndpoint function wraps net.Dial with a timeout to fail fast when the daemon is unreachable.
  • Read deadlines: Synchronous calls set a 30-second deadline on the socket read operation, ensuring the client does not block indefinitely if the daemon stalls.
  • Connection cleanup: Both the server and client ensure sockets are closed properly. The server's Close method triggers a shutdown signal that propagates to all active handleConn goroutines.

Practical Usage Examples

These patterns demonstrate how the CLI interacts with the daemon through the IPC socket.

Health Check Verification

The no-mistakes daemon health command uses this pattern from internal/cli/daemon_cmd.go:

sock := daemonPath.Socket()
client, err := ipc.Dial(sock)
if err != nil {
    log.Fatalf("cannot connect to daemon: %v", err)
}
defer client.Close()

var health struct{ OK bool }
if err := client.Call(ipc.MethodHealth, nil, &health); err != nil {
    log.Fatalf("health RPC failed: %v", err)
}
fmt.Printf("daemon health: %v\n", health.OK)

Subscribing to Run Events

To monitor a task's progress, the CLI opens a subscription as implemented in internal/daemon/manager.go:

params := &ipc.SubscribeParams{RunID: runID}
events, cancel, err := ipc.Subscribe(sock, params)
if err != nil {
    log.Fatalf("subscription failed: %v", err)
}
defer cancel()

for ev := range events {
    fmt.Printf("event: %s – %s\n", ev.Type, ev.Payload)
}

Executing Remote Commands

Invoking a lint command remotely follows this structure:

req := ipc.RunCommandParams{
    Command: "lint",
    Args:    []string{},
}
var result ipc.CommandResult
if err := client.Call(ipc.MethodRun, req, &result); err != nil {
    log.Fatalf("run command failed: %v", err)
}
fmt.Println("lint output:", string(result.Stdout))

Summary

  • The server in internal/ipc/server.go binds to <NM_HOME>/daemon.sock, accepts connections in handleConn, and dispatches JSON-RPC requests to handlers registered in internal/daemon/manager.go.
  • The client in internal/ipc/client.go dials the socket with timeout protection via Dial, executes synchronous calls through CallWithTimeout, and manages streaming subscriptions via Subscribe.
  • The protocol uses JSON-RPC 2.0 envelopes defined in internal/ipc/protocol.go, with built-in safeguards including connection timeouts, read deadlines, and graceful shutdown signaling.

Frequently Asked Questions

Where is the IPC socket file located?

The daemon creates the socket file at <NM_HOME>/daemon.sock within the daemon's home directory. The exact path is determined by the daemon manager and passed to CLI commands through environment variables or default path resolution.

How does the client prevent hanging if the daemon is unresponsive?

The client sets a configurable connection timeout via the NM_DAEMON_CONNECT_TIMEOUT environment variable during the dialEndpoint phase. Additionally, synchronous calls apply a 30-second read deadline (defaultCallTimeout) on the socket to detect stalled responses.

Can multiple clients subscribe to daemon events simultaneously?

Yes, each call to Subscribe opens a dedicated socket connection, allowing multiple CLI instances to receive real-time event streams independently without blocking each other.

What happens to active connections when the daemon shuts down?

When the server's Close method is invoked, it signals a done channel that causes all running handleConn goroutines to abort their current requests and return, ensuring clean termination without orphaned connections.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →