appserver

package
v0.18.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package appserver is the DAEMON-OWNED JSON-RPC client for `codex app-server --listen unix://PATH` (Wave R7, Mirror M4.2/M4.3; ADR-013 §R7.2d).

It holds the connection, the request-id correlation and the server-request delivery, and NOTHING ELSE. It imports neither internal/adapter nor internal/daemon: the adapter shapes content and owns no fd, the daemon owns lifecycle, and this package owns one socket.

THE SINGLE MOST IMPORTANT INTEGRATION DETAIL IN R7, and it cost the R1 feasibility gate ten minutes of its timebox to find (docs/verification/r1-codex-gate.md:32-37):

The `--listen unix://PATH` endpoint is a WEBSOCKET endpoint, not a raw JSON-lines
socket. A client must perform an HTTP/1.1 upgrade (GET /rpc, Upgrade: websocket,
Sec-WebSocket-Version: 13) over the UDS and then exchange JSON-RPC messages as
WebSocket TEXT frames. Writing bare newline-delimited JSON to the socket produces
TOTAL SILENCE: no response, no error, no server log.

`codex app-server proxy --sock` is NOT the bridge to that endpoint (gate correction 2). The recorded upgrade bytes are r1-codex-fixtures/ws-handshake.txt, captured off the real TUI's own connection through a logging MITM on the UDS.

TWO CORRELATION RULES THE RECORDED CORPUS FORCES, and a naive client gets both wrong:

  • "the next frame is my answer" is FALSE on the very first exchange of every session -- frame-samples.json shows a `remoteControl/status/changed` notification arriving between `initialize` and its reply -- so replies are matched BY ID.
  • an approval arrives as an UNSOLICITED SERVER REQUEST correlated to nothing the client sent, so a client that only routes frames carrying an id it is WAITING for drops every approval on the floor.

Index

Constants

View Source
const RPCPath = "/rpc"

RPCPath is the app-server's WebSocket endpoint. RECORDED: ws-handshake.txt's first line is `GET /rpc HTTP/1.1`.

Variables

View Source
var ErrClosed = errors.New("appserver: connection closed")

ErrClosed is returned to every caller parked on a connection that went away.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is one connection to one app-server.

func Dial

func Dial(ctx context.Context, socketPath string, opt Options) (*Client, error)

Dial connects to the app-server's UDS and performs the recorded WebSocket upgrade. It returns only once the connection is usable, and it FAILS rather than hanging when the peer accepts and then says nothing -- which is exactly what a wrong-protocol write produces.

func (*Client) Call

func (c *Client) Call(ctx context.Context, method string, params, out any) error

Call sends a request and blocks until its reply arrives, the context ends, or the connection closes. out, when non-nil, receives the result member.

func (*Client) CallAtWriteBoundary added in v0.13.11

func (c *Client) CallAtWriteBoundary(ctx context.Context, method string, params, out any, beforeWrite func() error, afterWrite func()) error

CallAtWriteBoundary is Call with an atomicity seam around the request write. beforeWrite runs after params are encoded and the reply waiter is installed, immediately before bytes are written -- and UNDER the connection's write lock, so no other frame on this connection can interpose between beforeWrite's durable record and this request's bytes. When it succeeds, afterWrite runs immediately after the write attempt (success or failure) and before waiting for the reply. The composer queue uses this narrow boundary to order durable Begin + request write against Stop without holding its lane lock for the potentially 30-second reply wait; ContextGuard's dispatch relies on the interposition guarantee (ADR-023 amendment 1). beforeWrite must not call back into this client.

func (*Client) Close

func (c *Client) Close() error

Close tears the connection down and unblocks every waiter.

func (*Client) Done

func (c *Client) Done() <-chan struct{}

Done is closed once the connection has ended.

func (*Client) Notify

func (c *Client) Notify(ctx context.Context, method string, params any) error

Notify sends a request that expects no reply (`initialized`).

func (*Client) Respond

func (c *Client) Respond(ctx context.Context, id json.RawMessage, result any) error

Respond answers a server-initiated request. The id MUST be the bytes that connection received: JSON-RPC ids are PER-CONNECTION, so a reply carrying any other id answers nothing and the agent stays blocked.

type Options

type Options struct {
	// DialTimeout bounds the connect and the HTTP upgrade (0 => defaultDialTimeout).
	DialTimeout time.Duration
	// OnNotify receives every server->client NOTIFICATION (a frame with a method and no id).
	// It runs on the read loop's goroutine, so it must not block for long.
	OnNotify func(method string, params json.RawMessage)
	// OnFrame receives untouched notification/request envelopes instead of the split callbacks.
	OnFrame func(method string, frame json.RawMessage)
	// OnRequest receives every server->client REQUEST (a frame with BOTH a method and an id).
	// The client must answer it with Respond, carrying the SAME id, on THIS connection.
	OnRequest func(id json.RawMessage, method string, params json.RawMessage)
	// OnClose fires exactly once when the read loop ends, with the reason.
	OnClose func(error)
}

Options configures a Dial.

type RPCError

type RPCError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

RPCError is a JSON-RPC error frame, surfaced TYPED rather than flattened to a string. Two of the three errors the gate recorded are load-bearing SEMANTICS rather than failures -- `no active turn to interrupt` is benign (§R7.5) and `no rollout found for thread id` is the join race (§R7.2e) -- and a caller that could not tell either from a transport fault would report a working Stop as a failure.

func (*RPCError) Error

func (e *RPCError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL