# How the IPC Layer Handles Communication and Health Checks with the Daemon

> Discover how the IPC layer ensures reliable CLI-to-daemon communication and fast health checks using JSON-RPC, configurable timeouts, and dedicated event streams in the no-mistakes repository.

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

---

**The `internal/ipc` package implements a JSON-RPC layer over Unix-domain sockets or Windows named pipes, using configurable dial timeouts, mutex-protected request serialization, and dedicated connections for event streaming to enable reliable CLI-to-daemon communication and sub-second health checks.**

The no-mistakes repository relies on a lightweight inter-process communication (IPC) mechanism to coordinate between the CLI client and a background daemon process. Understanding how the IPC layer handles communication and health checks with the daemon is essential for debugging connection failures and optimizing timeout behavior in production environments.

## Connection Establishment and Dial Timeouts

### Configuring Connection Timeouts

In [`internal/ipc/client.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/client.go), the `Dial` function establishes a transport connection via `dialEndpoint`, which creates either a Unix-domain socket or a Windows named pipe. The operation respects a configurable `connectTimeout` that first checks the `NM_DAEMON_CONNECT_TIMEOUT` environment variable, then falls back to `config.DefaultDaemonConnectTimeout` defined in [`internal/config/config.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go). If the dial operation exceeds this threshold, the function returns a `ConnectTimeoutError` that implements the standard `Timeout()` method, allowing callers to distinguish between network failures and bounded-connect timeouts.

### Fast Health-Check Timeouts

For liveness probes that require immediate feedback, the package exposes `client.DefaultDialTimeout` set to **250 ms**. This short deadline is applied as a read timeout during health checks in [`internal/daemon/selfexec.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/selfexec.go), ensuring the CLI can quickly detect an unresponsive daemon and spawn a new instance if necessary.

## JSON-RPC Request/Response Cycle

### Serialized Request Processing with Mutex Protection

The `Client` struct maintains a single `net.Conn`, a JSON encoder, a line-oriented scanner, and a mutex (`mu`) that guarantees only one request is in flight at a time. The `Call(method string, params interface{}, result interface{})` and `CallWithTimeout(method string, params interface{}, result interface{}, timeout time.Duration)` methods acquire this lock before writing to the socket. This prevents interleaving of request/response pairs on the same connection, ensuring reliable request-reply semantics even under concurrent access.

### Timeout Handling and Error Propagation

Each RPC builds a request using `NewRequest`, encodes it to the socket, then sets a read deadline based on `defaultCallTimeout` (**30 seconds** unless overridden). The response is read line-by-line from the scanner, unmarshaled into a `Response` struct defined in [`internal/ipc/protocol.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/protocol.go), and any JSON-RPC error is propagated as an `*RPCError`. This structured error handling allows the CLI to distinguish between transport failures and application-level RPC errors.

## Real-Time Event Streaming

For long-running notifications such as progress events during a run, the client uses the `Subscribe(socket string, params *SubscribeParams) (<-chan Event, func(), error)` function. Unlike standard RPC calls, this opens a **dedicated** connection separate from the main client channel, sends a `MethodSubscribe` request defined in [`internal/ipc/protocol.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/protocol.go), and validates the initial response.

A background goroutine reads subsequent lines from the socket, unmarshals them into `Event` structs, and forwards them on a buffered channel. The returned cancel function safely shuts down the scanner, closes the socket, and uses `sync.Once` to guarantee the event channel is closed exactly once, preventing panics from double-close operations.

```go
// Subscribe to run‑level events.
func watchRunEvents(socket string, runID string) (<-chan ipc.Event, func()) {
    params := &ipc.SubscribeParams{RunID: runID}
    ch, cancel, _ := ipc.Subscribe(socket, params)
    return ch, cancel
}

```

## Health Check Implementation

The daemon’s health endpoint in [`internal/daemon/selfexec.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/selfexec.go) performs a minimal liveness check by dialing the IPC socket with `DefaultDialTimeout`. If the connection succeeds and a basic ping RPC replies within the 250 ms deadline, the daemon is considered healthy. Because `Dial` respects the configurable connect timeout, health checks fail fast when the daemon is not listening or the socket is stale, triggering the CLI's auto-restart logic.

```go
// Simple health check – returns nil if the daemon is reachable.
func daemonHealthy(socket string) error {
    // Use the short default dial timeout for a quick probe.
    c, err := ipc.Dial(socket)
    if err != nil {
        return err
    }
    defer c.Close()

    // Perform a lightweight RPC, e.g., "ping".
    var pong string
    return c.CallWithTimeout(ipc.MethodPing, nil, &pong, ipc.DefaultDialTimeout)
}

```

## Summary

- The `internal/ipc` package provides a JSON-RPC transport over Unix-domain sockets or Windows named pipes, with configurable timeouts via `NM_DAEMON_CONNECT_TIMEOUT` and `config.DefaultDaemonConnectTimeout`.
- A mutex-protected `Client` in [`internal/ipc/client.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/client.go) ensures request-response serialization, preventing interleaving on shared connections.
- Health checks leverage a 250 ms `DefaultDialTimeout` for fast daemon liveness detection in [`internal/daemon/selfexec.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/selfexec.go).
- The `Subscribe` API uses dedicated connections and `sync.Once` for safe teardown of long-running event streams.
- All timeout errors implement standard interfaces (`Timeout()`) to allow callers to distinguish between network and application failures.

## Frequently Asked Questions

### How does the IPC layer prevent request interleaving on shared connections?

The `Client` struct in [`internal/ipc/client.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/client.go) protects the underlying `net.Conn` with a mutex (`mu`). Both `Call` and `CallWithTimeout` acquire this lock before writing to the socket, ensuring that only one request-response pair is in flight at a time. This serialization guarantees that JSON-RPC responses are correctly matched to their requests even when multiple goroutines attempt to use the same client concurrently.

### What timeout values does the No-Mistakes IPC layer use for health checks?

Health checks use `DefaultDialTimeout` (**250 ms**) as defined in [`internal/ipc/client.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/client.go). This value is applied as a read deadline when dialing the socket in [`internal/daemon/selfexec.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/selfexec.go). For general RPC calls, the default is `defaultCallTimeout` (**30 s**), while the initial connection timeout is controlled by the `NM_DAEMON_CONNECT_TIMEOUT` environment variable or falls back to `config.DefaultDaemonConnectTimeout`.

### How does the subscription API handle connection shutdown safely?

The `Subscribe` function returns a cancel closure that uses `sync.Once` to ensure the event channel is closed exactly once, even if invoked multiple times. This prevents panics from double-close operations and guarantees that the dedicated connection underlying the subscription is properly terminated, releasing system resources and preventing goroutine leaks.

### What is the difference between ConnectTimeoutError and RPCError?

`ConnectTimeoutError` is returned by `Dial` when the initial socket connection exceeds the configured timeout, implementing the `Timeout()` method for error classification. In contrast, `*RPCError` represents application-level JSON-RPC errors returned by the daemon after a successful connection, such as invalid method names or malformed parameters, and is decoded from the JSON response body defined in [`internal/ipc/protocol.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/ipc/protocol.go).