IPC Socket Coordination Between the no-mistakes Daemon and CLI

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): 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): 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): 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 & 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, where the daemon registers specific methods:

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

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

  • listen creates a net.UnixListener at the specified path
  • dial connects to the Unix domain socket

Windows Systems (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.

  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

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

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

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 manages connection lifecycles with cancellable contexts and goroutine-per-connection handling.
  • Client Safety: 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 and 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.

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 →