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

> Learn how the no-mistakes IPC socket facilitates daemon-client communication using JSON-RPC over a Unix-domain socket. Explore synchronous calls and streaming events.

- Repository: [Kun Chen/no-mistakes](https://github.com/kunchenguid/no-mistakes)
- Tags: internals
- Published: 2026-07-13

---

**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`](https://github.com/kunchenguid/no-mistakes/blob/main/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:

```go
// 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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`:

```go
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:

```go
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:

```go
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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/protocol.go). The protocol follows JSON-RPC 2.0 conventions with minimal extensions:

```go
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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd.go):

```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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/manager.go):

```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:

```go
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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/manager.go).
- The client in [`internal/ipc/client.go`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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.