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
- Variables
- type Client
- func (c *Client) Call(ctx context.Context, method string, params, out any) error
- func (c *Client) CallAtWriteBoundary(ctx context.Context, method string, params, out any, beforeWrite func() error, ...) error
- func (c *Client) Close() error
- func (c *Client) Done() <-chan struct{}
- func (c *Client) Notify(ctx context.Context, method string, params any) error
- func (c *Client) Respond(ctx context.Context, id json.RawMessage, result any) error
- type Options
- type RPCError
Constants ¶
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 ¶
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 ¶
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 ¶
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) Done ¶
func (c *Client) Done() <-chan struct{}
Done is closed once the connection has ended.
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 ¶
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.