Skip to content

Latest commit

 

History

History
787 lines (601 loc) · 35.5 KB

ipc.md

File metadata and controls

787 lines (601 loc) · 35.5 KB

IPC & Headless Mode

arf includes a built-in IPC (Inter-Process Communication) server that allows external tools to interact with a running R session. This enables AI agents, CI pipelines, and editor extensions to evaluate R code, query session state, and control the session programmatically.

Warning

IPC is an experimental feature. The protocol and CLI interface may change in future versions.

Overview

The IPC system has two parts:

  1. Server — Runs inside arf, listening on a Unix socket (Linux/macOS) or named pipe (Windows)
  2. Client — The arf ipc subcommands that connect to a running server

The server can be enabled in three ways:

Method Use case
arf headless CI, AI agents, background R sessions (no terminal needed)
arf --with-ipc Interactive REPL with external tool access
:ipc start meta command Enable IPC in an already-running session

Headless Mode

Headless mode starts R with an IPC server but without the interactive REPL. This is ideal for environments where no terminal is available.

arf headless

The server runs until interrupted by Ctrl+C, SIGTERM, or arf ipc shutdown. Immediately after startup there is a brief window where the IPC endpoint is listening but R is not yet ready, so early client requests may return R_BUSY. For scripted or automated use, prefer arf headless --json and treat the JSON emitted on stdout as the readiness signal, or retry the first arf ipc command with a small backoff until it succeeds.

Options

Option Description
--json Print session info as JSON to stdout when ready (implies --quiet)
--ipc-bind <PATH> Custom socket path (Unix) or named pipe path (Windows)
--ipc-pid-file <PATH> Write PID to file (removed on shutdown)
--log-file <PATH> Redirect log output to file instead of stderr
--history-dir <PATH> Override history database directory
--no-history Keep command history in memory for this session only (no disk load/save)
--quiet Suppress status messages on stderr
--config <PATH> Path to configuration file
--with-r-version <VER> R version to use via rig
--r-home <PATH> Explicit R_HOME path
--r-provider <PROVIDER> Explicitly select the current synced rig project or an active Pixi environment
--no-r-source-overrides Disable the configured R source override provider chain
--vanilla Start R without init files

PID file lifecycle (interactive and headless)

The PID file is the current-process pointer for an integration slot; it is not a readiness marker. Readiness is established by the IPC session metadata (or the --json output). Interactive :restart and :switch are a short transition: clients should use bounded retries, re-read the PID and session metadata, and detect the new generation by a changed started_at value. On Unix the replacement keeps ownership of a matching PID file. On Windows the parent relinquishes the file before spawning the replacement, so the file is briefly absent and then publishes a new PID. On Windows only, the default endpoint also changes because it contains the child PID; each retry must resolve it again. On Unix the default endpoint path remains the same across an exec restart, although it is temporarily unavailable during the restart window. In both cases, started_at changes for the new generation. An explicitly configured --ipc-bind path is reused by the replacement on both platforms. For an interactive session, a relative custom path is resolved once against arf's initial working directory, before R startup profiles can change it. Headless sessions follow the same pointer/readiness contract until shutdown.

JSON Output (--json)

When --json is specified, arf prints session connection info to stdout as a single JSON object once the server is ready:

{
  "pid": 12345,
  "socket_path": "/run/user/1000/arf/12345.sock",
  "r_version": "4.4.2",
  "r_home": "/opt/R/4.4.2/lib/R",
  "cwd": "/workspace",
  "started_at": "2026-03-22T10:00:00+09:00",
  "log_file": null,
  "history_session_id": 1742601600000000000,
  "ipc_policy": {
    "silent": {
      "mode": "restricted",
      "allowed_functions": ["+", "mean"]
    },
    "visible": {
      "mode": "approval_not_required"
    }
  },
  "history_runtime": {
    "state": "persistent",
    "path": "/home/user/.local/share/arf/history/r.db",
    "reason": null,
    "detail": null
  },
  "r_source_override": {
    "state": "applied",
    "provider": "toml-key",
    "file": "rproject.toml",
    "key": "project.r_version",
    "requested_version": "4.4",
    "resolved_version": "4.4.2"
  },
  "r_source_selection": {
    "provider": "rig",
    "selected_by": {
      "kind": "version_request",
      "requested_r_home": null,
      "requested_version": "4.4",
      "source": {
        "kind": "project_file",
        "name": null,
        "path": "/project/rproject.toml",
        "format": "toml",
        "key": "project.r_version"
      }
    },
    "metadata": null
  },
  "warnings": []
}

All keys are always present. r_version, r_home, and log_file may be null; history_session_id is null only when history initialization is unavailable. ipc_policy is always present in this readiness output: silent is either restricted with its complete sorted allowed_functions list (an empty restricted list still permits bare literals and identifiers) or unrestricted with no allowlist field. Because this is headless readiness output, visible has mode approval_not_required for both send and eval --visible. history_runtime reports persistent, configured volatile, fallback volatile, or unavailable; its path is a diagnostic path when one was requested (for example, for a persistent or fallback open, or an unavailable initialization), and detail contains an optional human-readable failure diagnostic. r_home is the R installation the session is using, or null when the session has no R. The r_source_override object is always present; its state is one of applied, not_configured, no_match, failed, disabled, or shadowed_by_cli, and its other fields are null unless an override was applied. warnings captures non-fatal startup issues (e.g., config parse errors or history fallback diagnostics) that would otherwise only appear on stderr.

r_source_selection reports the actual selected provider, its provenance, and provider metadata using the same fields as arf r resolve, for both configured overrides and explicit --r-provider or ARF_R_PROVIDER requests. Provider details such as the Pixi environment or the rig project manifest appear only there.

The legacy r_source_override object reports only the configured override chain. For a Pixi or rig-project override, provider is "pixi" or "rig-project", and file, key, and requested_version are null. resolved_version may be null for Pixi and is always null for rig-project, whose installation name is not treated as an exact R version. An explicit provider request does not mark the override as applied (shadowed_by_cli, or disabled when overrides were disabled). Pixi warnings use the same warnings array and never require an interactive confirmation.

The IPC r_version is measured from a live R session; arf r resolve reports resolved_version, a prediction made before R starts.

Output is pretty-printed when stdout is a terminal, compact when piped. This is useful in CI scripts:

arf headless --json | jq -r .socket_path

R Configuration in Headless Mode

Headless mode automatically configures R for non-interactive use:

  • Pager: Redirected to stdout (no interactive less)
  • Help: Forced to plain text (options(help_type = "text"))
  • Browser: Disabled (URLs are printed instead of opening a browser)
  • Graphics: Defaults to file-based devices (png/pdf) instead of X11
  • Save/Restore: Always --no-save --no-restore-data

IPC Subcommands

All arf ipc subcommands connect to a running arf session. If only one session is active, it is used automatically. When multiple sessions are running, use --pid to target a specific one.

Output Format

All IPC action subcommands (list, eval, send, session, shutdown, history) output JSON to stdout. Output is pretty-printed when stdout is a terminal, compact when piped. Errors are written to stderr as structured JSON:

{
  "error": {
    "code": "R_BUSY",
    "message": "R is busy",
    "hint": "R is executing code. Wait for it to finish, or use 'arf ipc session' to check status.",
    "data": null
  }
}

All four fields (code, message, hint, data) are always present. hint is null when no hint is available. data contains additional structured information (e.g. {"buffer": "..."} for USER_IS_TYPING) or null.

The code field is a string identifier for stable matching. Process exit codes indicate the error category:

Exit code Meaning
0 Success
2 IPC transport error (socket/pipe connection failed, connection-level read timeout)
3 Session resolution error (no session found, ambiguous PID)
4 JSON-RPC protocol error (R busy, user typing, server-side request timeout from --timeout, etc.)

Note: Transport-level timeouts (failing to connect to the socket or low-level read/write timeouts) produce exit code 2 (TRANSPORT_ERROR). Server-side evaluation timeouts triggered by arf ipc eval --timeout result in a JSON-RPC error response from the server, which the client reports as exit code 4.

Error code strings:

Code Exit Description
TRANSPORT_ERROR 2 Socket/pipe connection failed, connection-level read timeout, etc.
SESSION_NOT_FOUND 3 No session with the specified PID, or no sessions at all
SESSION_AMBIGUOUS 3 Multiple sessions running and --pid not specified
R_BUSY 4 R is executing code
R_NOT_AT_PROMPT 4 R is in browser/menu mode
INPUT_ALREADY_PENDING 4 Another IPC request is already queued
USER_IS_TYPING 4 User is typing in the REPL (see data fields below)
INCOMPLETE_INPUT 4 R code is syntactically incomplete and would enter the continuation prompt
R_EVAL_NOT_ALLOWED 4 Evaluation was rejected by the server-side syntactic policy
INPUT_NOT_APPROVED 4 Interactive send was not approved at the REPL prompt
EMPTY_RESPONSE 4 Server returned no result
PARSE_ERROR 4 Invalid JSON in request
INVALID_REQUEST 4 Not a valid JSON-RPC request
METHOD_NOT_FOUND 4 Unknown method name
INVALID_PARAMS 4 Invalid method parameters
INTERNAL_ERROR 4 Server internal error
PROTOCOL_ERROR 4 Other JSON-RPC error

USER_IS_TYPING error data

When the user is typing in the REPL, the data field contains:

Field Type Description
buffer string Current editor buffer content (capped at 1024 characters)
buffer_truncated boolean true if the buffer was truncated
buffer_original_length integer Full character count of the original buffer

arf ipc eval — Evaluate R Code

What decides the policy is whether a human can see the operation, not whether the session is headless or interactive. Silent eval leaves no trace a person watching the session would notice, so it is protected by an exact allowlist. send and eval --visible do surface in the session, so they are protected by human approval instead: an interactive session shows a confirmation prompt, and a headless session has no human to ask, so both headless send and headless eval --visible run immediately and are unrestricted.

History follows a related but separate rule: every IPC operation is recorded except silent eval in an interactive session, which is the one path that runs without leaving anything behind for the person at the prompt.

Plain (silent) IPC evaluation uses a best-effort tree-sitter-r policy in both interactive and headless sessions. The default allowlist is empty. Configure exact direct call targets at startup, for example --ipc-eval-allow-function mean or --ipc-eval-allow-function stats::median; nested calls must also be allowlisted. Bare literals and object references (bare identifiers) evaluate without any configuration. Extraction operators ($, [, and [[) remain allowlist-gated; enable them with repeated --ipc-eval-allow-function flags, as in this debugger-style inspection setup:

arf headless \
  --ipc-eval-allow-function '$' \
  --ipc-eval-allow-function '[' \
  --ipc-eval-allow-function '[[' &

Other R operators use their exact spelling (such as +) in the same list. Assignments, control flow, computed callees, the native pipe |>, and ::: are always rejected in restricted mode. Syntax errors and policy violations are rejected before R evaluation and history recording. --ipc-eval-unrestricted is a startup-only escape hatch. This policy is not an R sandbox and does not promise that an allowed function is non-mutating. Evaluating a bare identifier can itself run code by forcing a promise or triggering an active binding; this is allowed by default because IPC eval never permits assignment, so a caller cannot create such a binding through IPC eval.

The same policy is advertised as the ipc_policy object in the headless --json readiness output and live arf ipc session responses. The interactive :info pager reports whether the IPC server is enabled. When enabled, it shows the silent-eval mode, lists each configured target on its own line so the complete allowlist can be checked without horizontal scrolling, and states the current approval requirement for visible requests (send/eval --visible). When disabled, it omits the policy details.

Evaluates R code and returns the captured output. The code runs silently by default — output is not shown in the session. With --visible, an interactive session uses the same human approval as send; a headless session runs it immediately like send. The response fields differ between these two visible paths: headless visible evaluation still goes through the capture wrapper and populates value and error, while interactive visible evaluation runs through normal REPL evaluation and returns null for both fields. In the interactive case, printed values and errors are in stdout and stderr.

# Basic evaluation
arf headless \
  --ipc-eval-allow-function length \
  --ipc-eval-allow-function Sys.sleep \
  --ipc-eval-allow-function getwd &
arf ipc eval 'length("abc")'

# With timeout (milliseconds; Sys.sleep must be allowlisted at server startup)
arf ipc eval --timeout 10000 'Sys.sleep(5)'

# Also show output in the session (REPL or headless stdout).
# This runs where the session shows it, so it needs no allowlist entry.
arf ipc eval --visible 'cat("hello\n")'

# Target a specific session
arf ipc eval --pid 12345 'getwd()'

Parameters:

Parameter Description
<CODE> R code to evaluate (required)
--visible Also show output in the session. In an interactive session, this is governed by the same human approval as send, not the eval allowlist, and value/error are always null in the response. In headless mode, it runs immediately like send and still populates value/error.
--timeout <MS> Timeout in milliseconds for waiting for the response (default: 300000 = 5 minutes). This does NOT cancel the R evaluation — long-running code keeps R busy after timeout.
--pid <PID> Target session PID

Output format: JSON object with stdout (string), stderr (string), value (string or null), and error (string or null). All four fields are always present, but value and error are not populated for an interactive session's visible evaluation: normal REPL output and errors appear in stdout and stderr instead. Silent evaluation (the default) and visible evaluation in a headless session use the capture wrapper, so the printed result appears in value and R evaluation errors appear in error, with exit code 0 — they are normal responses, not IPC failures.

When the timeout fires, the server returns a JSON-RPC error response instead of a result, and the client prints that error as structured JSON on stderr and exits with code 4 — the timeout is therefore not reported in the result object's error field. The R evaluation continues, so the session stays busy until it finishes.

Example (silent eval, result captured in value):

{
  "stdout": "",
  "stderr": "",
  "value": "[1] 2",
  "error": null
}

Example (R error):

{
  "stdout": "",
  "stderr": "",
  "value": null,
  "error": "object 'x' not found"
}

arf ipc send — Send User Input

Sends code as if the user typed it at the prompt. Output goes to the session's output streams (REPL terminal or headless stdout/log file) and is not captured in the IPC response.

send is a visible, history-recorded operation. In an interactive REPL it is shown for human approval; in headless mode it runs immediately because there is no human to ask. It is not governed by the silent-eval allowlist.

In an interactive REPL, send displays the complete escaped code that will be executed after approval. Control and other non-printing characters are escaped so they cannot alter or spoof the terminal display. The approval block is shown as three separate lines and requires an explicit y or Y key for each request by default:

# [arf] IPC send request:
  <escaped code>
# [arf] Press y to approve, any other key declines:

The heading and # [arf] prefix are dark cyan, the escaped code is yellow, and the confirmation text is bold yellow. The three-line structure carries the meaning independently of color. Long code is not shortened; it remains available in the terminal scrollback, so the displayed escaped code corresponds to the complete code being approved.

Any other key, including Enter, n, Ctrl+C, Ctrl+D, or Esc, rejects the request. Non-key terminal events are ignored while waiting. A read error or a request whose client timeout has elapsed also rejects the request. This is controlled for the current process only with :ipc send-policy allow and restored with :ipc send-policy prompt; it cannot be changed by configuration or startup CLI options. Headless send remains immediate.

# Send code that appears in the session output
arf ipc send 'library(dplyr)'

# Target a specific session
arf ipc send --pid 12345 'print(mtcars)'

Output format: JSON object with accepted (bool). Example: {"accepted": true}

When to use eval vs send:

eval send
Output captured in response Yes No
Shown in session Only with --visible Always
User-configurable timeout (--timeout / timeout_ms) Yes No
Use case Programmatic access Human-visible interaction

arf ipc session — Get Session Info

Returns structured session information as JSON, including arf version, R environment details, and runtime state.

# Pretty-printed on terminal, compact when piped
arf ipc session

# Extract R version with jq
arf ipc session | jq -r '.r.version'

# Check loaded namespaces
arf ipc session | jq '.r.loaded_namespaces'

The response always has the same shape. When R is busy, the r field is null and r_unavailable_reason explains why:

{
  "arf_version": "0.2.6",
  "pid": 12345,
  "os": "linux",
  "arch": "x86_64",
  "r_home": "/opt/R/4.4.2/lib/R",
  "socket_path": "/run/user/1000/arf/12345.sock",
  "started_at": "2026-03-22T10:00:00+09:00",
  "log_file": null,
  "ipc_policy": {
    "silent": {
      "mode": "restricted",
      "allowed_functions": []
    },
    "visible": {
      "mode": "approval_not_required"
    }
  },
  "r": null,
  "r_unavailable_reason": "R is busy evaluating another expression",
  "hint": null
}

All top-level keys are always present. ipc_policy is the complete policy object described in the evaluation section; it is present even when R is busy. r_home is the R installation the session is using, or null when the session has no R. The r object contains information collected by evaluating R and may be null while R is busy or unavailable. arch is the architecture of the arf process, not the R installation.

The example above shows a headless session, so visible has mode approval_not_required; it covers both send and eval --visible. For an interactive session, this live policy reflects the current :ipc send-policy state: allow reports approval_not_required, while prompt reports approval_required.

When R is idle, the r field contains session details (other top-level fields omitted for brevity):

{
  "r": {
    "version": "4.4.1",
    "platform": "x86_64-pc-linux-gnu",
    "locale": "en_US.UTF-8",
    "cwd": "/workspace",
    "loaded_namespaces": ["base", "stats", "utils"],
    "attached_packages": ["base", "datasets"],
    "lib_paths": ["/usr/lib/R/library"]
  },
  "r_unavailable_reason": null,
  "hint": null
}

arf ipc list — List Active Sessions

Returns all running arf sessions with IPC enabled as JSON.

arf ipc list

# Example output:
# {
#   "sessions": [
#     {
#       "pid": 12345,
#       "r_version": "4.4.1",
#       "r_home": "/opt/R/4.4.1/lib/R",
#       "socket_path": "/run/user/1000/arf/12345.sock",
#       "cwd": "/workspace",
#       "started_at": "2026-03-22T10:00:00+09:00",
#       "session_type": "headless",
#       "log_file": null,
#       "history_session_id": 1742601600000000000
#     }
#   ]
# }

When no sessions are running, returns {"sessions": []} (exit 0).

The session_type field is "headless" for sessions started with arf headless and "interactive" for sessions running an interactive REPL. The field is always present in current session metadata.

The r_home field is the R installation the session is using. A null value means either that the session has no R, or that it was created by an older arf that did not record the path: a session file written before this field existed is listed with r_home set to null, like any other unset field. Clients must treat null as unknown and must not assume an R installation is available.

r_version, r_home, log_file, and history_session_id may be null. The list is discovery metadata only; it does not include the effective IPC policy. Query arf ipc session to obtain the live policy at the time of the request.

arf ipc history — Query Command History

Returns command history entries from the session's SQLite history database as JSON. By default, only entries from the current session are returned. This method is handled on the server thread and does not touch R, so it works even when R is busy.

# Show recent history from this session (default 50 entries)
arf ipc history

# Show last 10 entries
arf ipc history --limit 10

# Include history from all sessions (not just current)
arf ipc history --all-sessions

# Search for commands containing 'dplyr'
arf ipc history --grep dplyr

# Filter by working directory
arf ipc history --cwd /path/to/project

# Show entries since a date
arf ipc history --since 2026-03-29

# Combine filters
arf ipc history --grep 'library' --limit 20

# Extract commands with jq
arf ipc history | jq -r '.entries[].command'

Parameters:

Parameter Description
--limit <N> Maximum number of entries to return (default: 50, must be positive)
--all-sessions Include entries from all sessions, not just the current one
--cwd <PATH> Filter entries by exact working directory
--grep <PATTERN> Filter entries whose command contains this substring
--since <DATE> Only return entries after this timestamp (RFC 3339 or YYYY-MM-DD)
--pid <PID> Target session PID

Output format: JSON object with entries array (newest first) and session_id. Each entry contains command, timestamp, cwd, exit_status, and session_id (all fields are always present; null when not available). Output is pretty-printed when stdout is a terminal, compact when piped.

Note

Only completed commands are recorded in the history database. A command that is currently executing will not appear in the results until it finishes.

arf ipc shutdown — Shut Down Headless Session

Sends a graceful shutdown request to a headless session. The session cleans up (removes socket, PID file, session file) before exiting.

arf ipc shutdown
arf ipc shutdown --pid 12345

Output format: JSON object with accepted (bool). Example: {"accepted": true}

IPC in Interactive REPL

You can enable IPC in the interactive REPL without headless mode:

Using the --with-ipc Flag

arf --with-ipc

This starts the REPL normally and also starts the IPC server. External tools can then interact with your session while you continue working interactively.

Using Meta Commands

Within a running session, you can start and stop the IPC server:

:ipc start    # Start the IPC server
:ipc stop     # Stop the IPC server
:ipc status   # Show server status

Interactive + IPC: Mutual Exclusion

When both a human and an external tool use the same session, arf prevents conflicts:

  • If you are typing when an IPC eval or send request arrives, the request is rejected with a USER_IS_TYPING error
  • If an IPC eval or send request contains syntactically incomplete R code, it is rejected with an INCOMPLETE_INPUT error
  • If R is busy (not at the prompt), evaluate requests are rejected immediately with R_BUSY
  • If R is not at the prompt, user_input / send requests are rejected with R_NOT_AT_PROMPT
  • Clients are expected to handle these errors by retrying later (for example, with backoff). In interactive/REPL mode, the server accepts at most one pending request — additional requests are rejected with INPUT_ALREADY_PENDING. In headless mode, requests are queued and processed sequentially
  • The session and history methods do not touch R and can be called even when R is busy or not at the prompt. session always succeeds; history may fail only when no history owner is available or the store cannot be queried
  • list reads local session files and does not connect to any server, so it always works regardless of R state

Transport & Security

Unix (Linux/macOS)

The IPC server listens on a Unix domain socket. The default path depends on the platform:

  • When $XDG_RUNTIME_DIR is set: $XDG_RUNTIME_DIR/arf/<PID>.sock (typically /run/user/<UID>/arf/<PID>.sock on Linux with systemd)
  • When $XDG_RUNTIME_DIR is not set (e.g. macOS): <temp_dir>/arf-<random>/<PID>.sock (where <temp_dir> is the system temp directory, e.g. $TMPDIR; on Linux this is typically /tmp)

The socket directory and file are created with restrictive permissions:

  • Socket directory: mode 0700 (owner only)
  • Socket file (<PID>.sock): mode 0600 (owner only)

Before using the socket directory, arf validates that it is not a symlink, is owned by the current user, and is not writable by group or other users. If validation fails, arf attempts to use a per-process fallback directory instead. If all candidate directories are unsafe or cannot be created, IPC does not start and arf returns an error rather than continuing without IPC.

Session metadata JSON files (<PID>.json) are stored separately in the OS cache directory (e.g., ~/.cache/arf/sessions/ on Linux):

  • Directory: mode 0700 (owner only)
  • File: mode 0600 (owner only)

This prevents other users on the system from discovering or connecting to your session.

Windows

The IPC server listens on a named pipe:

\\.\pipe\arf-ipc-<PID>

Note

Although Windows 10 1803+ supports AF_UNIX sockets, arf currently uses named pipes on Windows because the async runtime (tokio) does not yet support AF_UNIX on Windows. See tokio#2201 for upstream progress.

Custom Bind Path

Use --ipc-bind to specify a custom socket/pipe path. This works for both headless and interactive REPL (--with-ipc) modes:

# Headless — Unix
arf headless --ipc-bind /tmp/my-arf.sock

# Headless — Windows
arf headless --ipc-bind \\.\pipe\my-arf

# Interactive REPL — Unix (editor launches arf and knows the socket path upfront)
arf --with-ipc --ipc-bind /tmp/my-arf.sock

Note

On Unix, the socket file is created with bind() and then restricted to 0600. There is a brief window between these two calls where the default umask applies. If you use a custom path, ensure the parent directory is user-private (e.g., mode 0700) to prevent other users from connecting during that window.

Session Discovery

Each arf session with IPC enabled writes a session file to the OS cache directory (e.g., ~/.cache/arf/sessions/<PID>.json on Linux, ~/Library/Caches/arf/sessions/<PID>.json on macOS). The session file contains discovery metadata such as the socket path so that arf ipc client commands can discover running sessions; it does not store the effective IPC policy. Query arf ipc session for the live policy at request time. Stale session files (where the process is no longer running) are automatically cleaned up.

Remote Access (No Built-in TCP)

arf intentionally does not listen on TCP. Supporting TCP would require building authentication and encryption into arf itself, which is better handled by dedicated tools. Instead, use an existing proxy or tunnel to expose the local socket remotely:

  • SSH tunneling (OpenSSH 6.7+ supports Unix socket forwarding) — recommended for most cases, as it provides encryption and authentication with no extra software
  • socat — lightweight bidirectional relay between Unix sockets and TCP, useful for quick bridging on trusted networks
  • Reverse proxies (Caddy, nginx) — suitable for persistent setups, especially when TLS is required

On Windows, arf listens on a named pipe which these tools cannot target directly. In WSL environments, npiperelay can bridge a Windows named pipe to stdin/stdout, allowing it to be combined with socat or SSH.

JSON-RPC Protocol

For tool developers who want to communicate with arf directly (without the arf ipc CLI), the server speaks JSON-RPC 2.0 over HTTP on the Unix socket or named pipe.

The server also accepts raw JSON request bodies (without HTTP request-line/headers) for simpler clients. Responses are still sent as standard HTTP/1.1 200 OK messages with headers followed by a JSON body, so raw-JSON clients need to strip the HTTP headers before parsing the response.

Request Format

Send an HTTP POST request with a JSON-RPC body:

POST / HTTP/1.1
Host: localhost
Content-Type: application/json
Content-Length: ...
Connection: close

{"jsonrpc": "2.0", "id": 1, "method": "evaluate", "params": {"code": "1 + 1"}}

Available Methods

Method Parameters Description
evaluate code (string), visible (bool, default false), timeout_ms (int, optional) Evaluate R code and return captured output
user_input code (string) Send code as user input
session (none) Get session information
history limit (int, default 50), all_sessions (bool, default false), cwd (string, optional), grep (string, optional), since (string, optional) Query command history
shutdown (none) Shut down the session (headless mode only; returns an error in interactive mode)

Response Examples

Successful evaluation:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "stdout": "",
    "stderr": "",
    "value": "[1] 2"
  }
}

R error:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "stdout": "",
    "stderr": "",
    "error": "object 'x' not found"
  }
}

Errors are caught by tryCatch, so the error message appears in the error field (via conditionMessage()). The stderr field is typically empty for caught errors.

R is busy:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32000,
    "message": "R is busy"
  }
}

Output Capture

The evaluate method captures R output through two separate channels:

  • stdout / stderr (console output): Captured via R's WriteConsoleEx callback at the C level. This includes text produced by cat(), message(), warning(), and other writes to the R console.
  • value / error (structured result): Captured separately as the evaluated result (or error) of the last expression, using a binary protocol (charToRaw() + writeBin() to a temp file, then read from Rust). In silent evaluate calls, printed values of expressions appear here rather than in stdout/stderr.

The result fields in the JSON response are already properly escaped strings — tool developers do not need to handle the raw binary protocol themselves.

Error Codes

Code Name Description
-32700 Parse Error Invalid JSON
-32600 Invalid Request Not a valid JSON-RPC request
-32601 Method Not Found Unknown method name
-32602 Invalid Params Invalid method parameters
-32603 Internal Error Server internal error
-32000 R Busy R is executing code
-32001 R Not At Prompt R has not returned to the prompt
-32002 Input Already Pending Another IPC request is already queued
-32003 User Is Typing User is typing in the REPL (interactive mode only)
-32004 Incomplete Input R code is syntactically incomplete

Troubleshooting

"No active arf sessions found"

The arf ipc client could not find a session file in the cache directory. This means:

  • No arf session has IPC enabled, or
  • The session file could not be created (for example, the cache directory is missing or not writable), or
  • The session file was cleaned up (the process exited)

Fix: Start arf with arf headless or arf --with-ipc.

"R is busy"

In interactive/REPL mode, the request is rejected immediately with R_BUSY — it is not queued. In headless mode, requests are queued and processed sequentially; clients will typically block until the current operation finishes or their own timeout elapses.

Fix: For interactive mode, handle R_BUSY responses by retrying the request with backoff. In headless mode, configure appropriate client-side timeouts. Note that --timeout only limits how long the IPC call waits for a reply — it does not cancel the underlying R evaluation, and long-running code may keep R busy even after the client times out.

"User is typing" (interactive mode)

In interactive REPL mode, IPC requests are rejected when the user has text in the editor buffer.

Fix: Clear the input line or press Enter before sending the IPC request. This protection prevents IPC from disrupting the user's typing.

Connection refused / timeout

The socket exists but the server is not responding.

Possible causes:

  • The arf process crashed but the socket file was not cleaned up
  • R is stuck in an infinite loop or blocking operation

Fix: Check if the process is still running with arf ipc list. If the session is stale, remove the socket file shown in the socket_path field (for example, $XDG_RUNTIME_DIR/arf/<PID>.sock or <temp_dir>/arf-<random>/<PID>.sock on Unix) and the session metadata file (~/.cache/arf/sessions/<PID>.json).

Permission denied on socket

On Unix, the socket directory and files are created with restrictive permissions (mode 0700/0600). If you see permission errors:

  • Ensure you are connecting as the same user who started arf
  • Check that the socket directory ($XDG_RUNTIME_DIR/arf/ or <temp_dir>/arf-<random>/) has the correct ownership