# IPC Socket Coordination Between the no-mistakes Daemon and CLI

> Discover how the no-mistakes daemon and CLI coordinate IPC sockets using JSON-RPC 2.0 over Unix domain sockets or Windows named pipes. Learn about their communication mechanism.

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

---

**The no-mistakes daemon and CLI communicate via a JSON-RPC 2.0 protocol over Unix domain sockets (or Windows named pipes), using an `ipc.Server` listener and `ipc.Client` dialer that marshal requests and responses through platform-specific transports in `internal/ipc/`.**

The `no-mistakes` project implements a robust inter-process communication (IPC) layer that bridges the background daemon with command-line interface tools. Understanding how IPC socket coordination works is essential for developers extending the CLI or debugging connection issues. The implementation resides in the `internal/ipc` package and supports both Unix domain sockets and Windows named pipes through a unified abstraction.

## Architecture of the IPC Layer

The IPC system is built on four primary abstractions that separate concerns between transport, protocol, and application logic.

### Core Components

- **`Server`** ([`internal/ipc/server.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/server.go)): Listens on the socket path and dispatches incoming JSON-RPC calls to registered handlers. It maintains a map of method handlers (`handlers`) and separate streaming handlers (`streamHandlers`).

- **`Client`** ([`internal/ipc/client.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/client.go)): Opens connections to the daemon, encodes requests through `Call` and `CallWithTimeout`, and decodes responses. The client serializes concurrent calls with a `sync.Mutex` and provides a `Subscribe` helper for event streaming.

- **`Protocol`** ([`internal/ipc/protocol.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/protocol.go)): Defines JSON-RPC method names (e.g., `push_received`, `get_run`, `subscribe`, `health`), request/response structures, and wire-type representations (`RunInfo`, `Event`). It also includes an atomic request-ID generator (`reqID`).

- **Transport Layer** ([`internal/ipc/transport_unix.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/transport_unix.go) & [`transport_windows.go`](https://github.com/kunchenguid/no-mistakes/blob/main/transport_windows.go)): Platform-specific implementations of `listen` and `dial` functions. On Unix, these create a `net.UnixListener` at the socket path; on Windows, they create a named pipe at `\\.\pipe\no-mistakes-ipc-<hash>`.

## How the IPC Server Handles Incoming Connections

The daemon initializes the IPC server early in its lifecycle using `ipc.NewServer()`. The server runs an accept loop (`Serve`) that spawns a goroutine for each incoming connection and cancels contexts when the server shuts down.

Handler registration occurs in [`internal/daemon/manager.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/manager.go), where the daemon registers specific methods:

```go
srv := ipc.NewServer()
srv.Handle(ipc.MethodHealth, func(ctx context.Context, _ json.RawMessage) (interface{}, error) {
    return ipc.HealthResult{Status: "alive"}, nil
})
// Additional handlers registered via Server.Handle or Server.HandleStream
go srv.Serve(socketPath) // Blocks until Close is called

```

The `handleConn` function in [`internal/ipc/server.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/server.go) creates a cancellable context (`ctx, cancel := context.WithCancel(context.Background())`) to ensure ongoing RPC calls abort cleanly when the daemon stops.

## How the CLI Client Connects to the Daemon

Before issuing commands that require daemon interaction, the CLI opens a client connection using `ipc.Dial(socketPath)`. The socket path is derived from the **NM_HOME** directory via `paths.New().IPCPath()`.

The `dialEndpoint` helper respects the `NM_DAEMON_CONNECT_TIMEOUT` environment variable to bound connection attempts, falling back to global configuration if unset.

For standard RPC calls, the client uses `Call` or `CallWithTimeout`:

```go
sockPath := paths.New().IPCPath()
cli, err := ipc.Dial(sockPath)
if err != nil {
    // Handle connection error (e.g., ConnectTimeoutError)
}
var health ipc.HealthResult
if err := cli.Call(ipc.MethodHealth, ipc.HealthParams{}, &health); err != nil {
    // Handle RPC error
}

```

The client marshals requests using `NewRequest` with auto-incremented IDs, writes JSON lines to the socket, and unmarshals responses into the caller-provided value. A read deadline (`c.conn.SetReadDeadline`) prevents indefinite blocking if the daemon hangs.

## JSON-RPC Protocol and Wire Format

The protocol file defines the contract between daemon and client. Key method constants include `MethodHealth`, `MethodPushReceived`, `MethodSubscribe`, and `MethodGetRun`.

Request and response structures follow JSON-RPC 2.0 specifications:

- **Request**: Contains `ID`, `Method`, and `Params` fields
- **Response**: Contains `ID`, `Result`, and optional `Error` fields
- **Event**: Streaming messages for real-time updates

The atomic request ID generator ensures unique identifiers for concurrent requests without collisions.

## Platform-Specific Transport Implementation

The transport layer abstracts operating system differences while exposing identical signatures to the server and client.

**Unix Systems** ([`transport_unix.go`](https://github.com/kunchenguid/no-mistakes/blob/main/transport_unix.go)):
- `listen` creates a `net.UnixListener` at the specified path
- `dial` connects to the Unix domain socket

**Windows Systems** ([`transport_windows.go`](https://github.com/kunchenguid/no-mistakes/blob/main/transport_windows.go)):
- `listen` creates a named pipe at `\\.\pipe\no-mistakes-ipc-<hash>`
- `dial` connects to the named pipe using Windows-specific APIs

Both implementations return standard `net.Conn` interfaces, allowing the higher-level JSON-RPC code to remain platform-agnostic.

## End-to-End Coordination Flow

The complete IPC socket coordination flow follows these steps:

1. **Daemon Initialization**: The daemon creates an IPC server using the socket path from `paths.New().IPCPath()` and begins listening in [`internal/daemon/manager.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/manager.go).

2. **Handler Registration**: The daemon registers handlers for each RPC method via `Server.Handle` or `Server.HandleStream`, mapping method names to Go functions.

3. **CLI Connection**: The CLI creates a client with `ipc.Dial(socketPath)`, respecting connection timeout configurations.

4. **Request Serialization**: The client builds a `Request` with an atomic ID, marshals parameters, and writes the JSON line to the socket using `encoder.Encode`.

5. **Server Dispatch**: The server reads the line, decodes it into a `Request`, looks up the handler in the dispatch map, invokes it, and encodes the `Response` back to the socket.

6. **Response Handling**: The client reads the next line, unmarshals the `Response`, checks for error fields, and unmarshals the `Result` into the target structure.

7. **Streaming Events**: For real-time updates, the CLI calls `ipc.Subscribe`, which opens a dedicated connection, sends a `MethodSubscribe` request, and spawns a goroutine that reads subsequent `Event` messages into a channel. The server's `HandleStream` logic sends an initial OK response, then writes events via the provided `send` callback.

## Practical Code Examples

### Checking Daemon Health

```go
sockPath := paths.New().IPCPath()
cli, err := ipc.Dial(sockPath)
if err != nil {
    log.Fatal(err)
}
defer cli.Close()

var health ipc.HealthResult
if err := cli.Call(ipc.MethodHealth, ipc.HealthParams{}, &health); err != nil {
    log.Fatal(err)
}
fmt.Println("Daemon status:", health.Status)

```

### Subscribing to Run Events

```go
params := &ipc.SubscribeParams{RunID: runID}
events, cancel, err := ipc.Subscribe(sockPath, params)
if err != nil {
    log.Fatal(err)
}
defer cancel()

for ev := range events {
    // Process ev.Type, update UI, etc.
    fmt.Printf("Received event: %s\n", ev.Type)
}

```

### Daemon Handler Registration

```go
srv := ipc.NewServer()
srv.Handle(ipc.MethodHealth, func(ctx context.Context, _ json.RawMessage) (interface{}, error) {
    return ipc.HealthResult{Status: "alive"}, nil
})

// Start serving on the IPC socket
go srv.Serve(socketPath)

```

## Summary

- **JSON-RPC 2.0 Protocol**: The system uses standard JSON-RPC over Unix domain sockets or Windows named pipes for cross-platform compatibility.
- **Server Architecture**: [`internal/ipc/server.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/server.go) manages connection lifecycles with cancellable contexts and goroutine-per-connection handling.
- **Client Safety**: [`internal/ipc/client.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/client.go) serializes concurrent calls with mutexes and enforces read deadlines to prevent CLI blocking.
- **Streaming Support**: Event subscriptions use dedicated connections with `HandleStream` for real-time TUI updates.
- **Configuration**: Socket paths derive from **NM_HOME**, while `NM_DAEMON_CONNECT_TIMEOUT` controls connection behavior.

## Frequently Asked Questions

### What transport protocols does no-mistakes use for IPC?

The project uses Unix domain sockets on Linux and macOS, and Windows named pipes on Windows. The transport implementations in [`internal/ipc/transport_unix.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/transport_unix.go) and [`transport_windows.go`](https://github.com/kunchenguid/no-mistakes/blob/main/transport_windows.go) expose identical `listen` and `dial` signatures, allowing the JSON-RPC layer to remain platform-agnostic.

### How does the CLI handle daemon connection timeouts?

The CLI respects the `NM_DAEMON_CONNECT_TIMEOUT` environment variable, falling back to global configuration if unset. The `dialEndpoint` helper uses this value to bound connection attempts, returning a `ConnectTimeoutError` if the daemon does not respond within the specified duration.

### Can multiple CLI instances connect to the same daemon simultaneously?

Yes. The `ipc.Server` implementation spawns a new goroutine for each accepted connection, maintaining independent contexts for request handling. Multiple CLI processes can dial the same socket path and issue concurrent requests serialized by each client's `sync.Mutex`.

### How are real-time events streamed from daemon to CLI?

The CLI calls `ipc.Subscribe`, which opens a dedicated connection and sends a `MethodSubscribe` request. The server handles this via `HandleStream`, sending an initial OK response followed by连续的 `Event` messages. The client spawns a goroutine that forwards these events on a Go channel, allowing the TUI to render updates without blocking the main thread.