# RPC

> Drive Nodecode from a program: one command a line on stdin, one frame a line on stdout.

Source: https://nodecode.ai/docs/rpc
Updated: 2026-10-08

A program spawns `nodecode rpc` and keeps its stdin open. Nothing here is a second protocol: a line in is the gateway's own command, and a line out is the gateway's own frame.

## Start it

```sh
nodecode rpc [SESSION-ID [--cursor N]] [--store PATH | --ephemeral]
```

It finds the organism serving your home, or starts one. It presents the operator token and subscribes to a session: the one you name, or a fresh one. Your program never handles a port, a token or a handshake. The first line out is the session's replay. Then come its facts as they land, the command's result and the turn's end.

```text
$ nodecode rpc
→ {"type":"start_turn","request":{"prompt":"say pong"}}
← {"sequence":1,"session_id":"s-GW3YSGSZ","payload":{"type":"session_stream_resumed","session_id":"s-GW3YSGSZ","accumulated_frames":[],"admission_state":"ready","replay_chunk_index":0,"replay_chunk_final":true},"cursor":0}
← {"sequence":0,"payload":{"sessions":[],"type":"organism.activity"}}
← {"sequence":2,"session_id":"s-GW3YSGSZ","payload":{"frame":{"seq":2,"session_id":"s-GW3YSGSZ","turn_id":false,"event":{"type":"session.created","payload":{"id":"s-GW3YSGSZ","cwd":"/home/you/project","parent":false,"created-at":"2026-10-09T02:53:33.203Z"},"metadata":{"event_id":"event-1U5SO19-6ME7O12TOP","log_position":2,"turn_id":false,"command_id":"command-1U5SO19-DVTNEZ5SG4","occurred_at":"2026-10-09T02:53:33.203Z"}}},"type":"session_stream_event"},"cursor":2,"durable":true}
← {"sequence":3,"session_id":"s-GW3YSGSZ","payload":{"frame":{"seq":3,"session_id":"s-GW3YSGSZ","turn_id":"turn-1U5SO19-KHY4SY503B","event":{"type":"turn.input_committed","payload":{"role":"user","message":"say pong","prompt-id":"command-1U5SO19-DFKDHVX7KL","sequence":1,"disposition":"started"},"metadata":{"event_id":"event-1U5SO19-QZP020Q1FF","log_position":3,"turn_id":"turn-1U5SO19-KHY4SY503B","command_id":"command-1U5SO19-DFKDHVX7KL","occurred_at":"2026-10-09T02:53:33.207Z"}}},"type":"session_stream_event"},"cursor":3,"durable":true}
← {"sequence":4,"session_id":"s-GW3YSGSZ","payload":{"frame":{"seq":4,"session_id":"s-GW3YSGSZ","turn_id":"turn-1U5SO19-KHY4SY503B","event":{"type":"turn.started","payload":{"input":"say pong"},"metadata":{"event_id":"event-1U5SO19-OOOH5SCVD9","log_position":4,"turn_id":"turn-1U5SO19-KHY4SY503B","command_id":"command-1U5SO19-DFKDHVX7KL","occurred_at":"2026-10-09T02:53:33.208Z"}}},"type":"session_stream_event"},"cursor":4,"durable":true}
← {"sequence":0,"payload":{"sessions":[{"id":"s-GW3YSGSZ","started_at":"2026-10-09T02:53:33.208Z"}],"type":"organism.activity"}}
← {"sequence":0,"payload":{"type":"gateway_turn_command_result","result":{"command_id":"command-1U5SO19-DFKDHVX7KL","correlation_id":"command-1U5SO19-DFKDHVX7KL","session_id":"s-GW3YSGSZ","command":"start_turn","status":"accepted","message":"turn admitted","response":{"turn_id":"turn-1U5SO19-KHY4SY503B","queue_depth":0},"message_id":"command-1U5SO19-DFKDHVX7KL","sequence":1}},"session_id":"s-GW3YSGSZ"}
← {"sequence":5,"session_id":"s-GW3YSGSZ","payload":{"turn_id":"turn-1U5SO19-KHY4SY503B","delta":{"type":"text","text":"pon"},"type":"item_delta"}}
← {"sequence":6,"session_id":"s-GW3YSGSZ","payload":{"turn_id":"turn-1U5SO19-KHY4SY503B","delta":{"type":"text","text":"g"},"type":"item_delta"}}
← {"sequence":7,"session_id":"s-GW3YSGSZ","payload":{"frame":{"seq":9,"session_id":"s-GW3YSGSZ","turn_id":"turn-1U5SO19-KHY4SY503B","event":{"type":"turn.assistant_message_completed","payload":{"role":"assistant","message":{"role":"assistant","content":"pong"}},"metadata":{"event_id":"event-1U5SO19-8SQB2KSYCH","log_position":9,"turn_id":"turn-1U5SO19-KHY4SY503B","command_id":"command-1U5SO19-DFKDHVX7KL","occurred_at":"2026-10-09T02:53:33.950Z"}}},"type":"session_stream_event"},"cursor":9,"durable":true}
← {"sequence":8,"session_id":"s-GW3YSGSZ","payload":{"frame":{"seq":10,"session_id":"s-GW3YSGSZ","turn_id":"turn-1U5SO19-KHY4SY503B","event":{"type":"turn.usage","payload":{"input-tokens":0,"output-tokens":8,"cached-input-tokens":940,"total-tokens":948,"cost_known":false,"cost_usd":0.0,"finish-reason" …192 characters… t-1U5SO19-2LCDPPIEOV","log_position":10,"turn_id":"turn-1U5SO19-KHY4SY503B","command_id":"command-1U5SO19-DFKDHVX7KL","occurred_at":"2026-10-09T02:53:33.952Z"}}},"type":"session_stream_event"},"cursor":10,"durable":true}
← {"sequence":9,"session_id":"s-GW3YSGSZ","payload":{"frame":{"seq":11,"session_id":"s-GW3YSGSZ","turn_id":"turn-1U5SO19-KHY4SY503B","event":{"type":"turn.completed","payload":{"turn-number":1,"message-count":2},"metadata":{"event_id":"event-1U5SO19-VFATPLPM63","log_position":11,"turn_id":"turn-1U5SO19-KHY4SY503B","command_id":"command-1U5SO19-DFKDHVX7KL","occurred_at":"2026-10-09T02:53:33.954Z"}}},"type":"session_stream_event"},"cursor":11,"durable":true}
· stdin closed
← {"sequence":0,"payload":{"sessions":[],"type":"organism.activity"}}
(exit 0)
```

One line in, thirteen lines out. The command's result came at 0.12 s, the turn's text at 0.81 s.

Mistakes to avoid:

- Reading the port from the store's `.owner` file and doing the WebSocket handshake yourself. The challenge, the operator token, the session and the subscription are what `rpc` does for you. Instead: Spawn `nodecode rpc` and write commands.
- `nodecode rpc serve`. Exit 2: `rpc is a command of its own: nodecode [-p NAME] rpc [SESSION-ID [--cursor N]] [--store PATH | --ephemeral]` Instead: Run `nodecode serve`, or `nodecode gateway on`, by itself. `nodecode rpc` attaches to it.
- Spawning one `nodecode rpc` per prompt. With no organism running, each one starts an organism and stops it when its stdin ends. Every start after a stop sends the model a "running again" note: 6 of the 20 model calls in the runs on this page. Instead: Keep one `nodecode rpc` open for your program's life, or run `nodecode gateway on` so each one attaches.

## Lines in

Each line is one command object. A line that names no `command_id` gets one, and a command that names no `session_id` goes to rpc's session. A line that is not a command gets a sync_protocol_error, and the next line is read.

| Type | Takes | Does |
| --- | --- | --- |
| `start_turn` | `request.prompt` | Starts a turn. One already running queues it. |
| `steer_turn` | `request.prompt`, `request.expected_turn_id` | The prompt runs at the running turn's next round boundary. |
| `cancel_turn` | `turn_id`, `retract` | Stops the running turn. `retract` also takes it back when it produced nothing. |
| `withdraw_prompt` | `prompt_id` | Takes a queued prompt back. |
| `interrupt_exec` | `exec_id` | Stops a background evaluation. |
| `navigate_checkpoint` | `target_turn_id` or `direction` | Moves the session's head. Every client is told to subscribe again. |
| `get_state` | nothing | The session as it stands, in the result. |
| `set_session_subscriptions` | `subscriptions` | The sessions this client follows. |

```text
$ nodecode rpc s-LINES
← {"sequence":1,"session_id":"s-LINES","payload":{"type":"session_stream_resumed","session_id":"s-LINES","accumulated_frames":[],"admission_state":"ready","replay_chunk_index":0,"replay_chunk_final":true},"cursor":0}
← {"sequence":0,"payload":{"sessions":[],"type":"organism.activity"}}
→ say pong
← {"sequence":0,"payload":{"code":"invalid-json","message":"invalid JSON: malformed JSON at byte 0: #\\s begins no value","permanent":false,"type":"sync_protocol_error"}}
→ {"type":"start_turn","prompt":"say pong"}
← {"sequence":0,"payload":{"code":"invalid-envelope","message":"request must be an object","permanent":false,"type":"sync_protocol_error"}}
→ {"prompt":"say pong"}
← {"sequence":0,"payload":{"code":"invalid-envelope","message":"type must be a non-empty string","permanent":false,"type":"sync_protocol_error"}}
→ {"type":"start_turn","request":{"prompt":"say pong"}}
← {"sequence":2,"session_id":"s-LINES","payload":{"frame":{"seq":43,"session_id":"s-LINES","turn_id":false,"event":{"type":"session.created","payload":{"id":"s-LINES","cwd":"/home/you/project","parent":false,"created-at":"2026-10-09T02:53:40.704Z"},"metadata":{"event_id":"event-1U5SO1G-KBPC0G11T5","log_position":43,"turn_id":false,"command_id":"command-1U5SO1G-HG053PED2F","occurred_at":"2026-10-09T02:53:40.704Z"}}},"type":"session_stream_event"},"cursor":43,"durable":true}
← {"sequence":3,"session_id":"s-LINES","payload":{"frame":{"seq":44,"session_id":"s-LINES","turn_id":"turn-1U5SO1G-4W89TJM6HX","event":{"type":"turn.input_committed","payload":{"role":"user","message":"say pong","prompt-id":"command-1U5SO1G-D5YKUGNF62","sequence":1,"disposition":"started"},"metadata":{"event_id":"event-1U5SO1G-DQDU9HH7V3","log_position":44,"turn_id":"turn-1U5SO1G-4W89TJM6HX","command_id":"command-1U5SO1G-D5YKUGNF62","occurred_at":"2026-10-09T02:53:40.705Z"}}},"type":"session_stream_event"},"cursor":44,"durable":true}
← {"sequence":4,"session_id":"s-LINES","payload":{"frame":{"seq":45,"session_id":"s-LINES","turn_id":"turn-1U5SO1G-4W89TJM6HX","event":{"type":"turn.started","payload":{"input":"say pong"},"metadata":{"event_id":"event-1U5SO1G-KXBB7XVR6V","log_position":45,"turn_id":"turn-1U5SO1G-4W89TJM6HX","command_id":"command-1U5SO1G-D5YKUGNF62","occurred_at":"2026-10-09T02:53:40.705Z"}}},"type":"session_stream_event"},"cursor":45,"durable":true}
← {"sequence":0,"payload":{"sessions":[{"id":"s-LINES","started_at":"2026-10-09T02:53:40.705Z"}],"type":"organism.activity"}}
← {"sequence":0,"payload":{"type":"gateway_turn_command_result","result":{"command_id":"command-1U5SO1G-D5YKUGNF62","correlation_id":"command-1U5SO1G-D5YKUGNF62","session_id":"s-LINES","command":"start_turn","status":"accepted","message":"turn admitted","response":{"turn_id":"turn-1U5SO1G-4W89TJM6HX","queue_depth":0},"message_id":"command-1U5SO1G-D5YKUGNF62","sequence":1}},"session_id":"s-LINES"}
← {"sequence":5,"session_id":"s-LINES","payload":{"turn_id":"turn-1U5SO1G-4W89TJM6HX","delta":{"type":"text","text":"pon"},"type":"item_delta"}}
← {"sequence":6,"session_id":"s-LINES","payload":{"turn_id":"turn-1U5SO1G-4W89TJM6HX","delta":{"type":"text","text":"g"},"type":"item_delta"}}
← {"sequence":7,"session_id":"s-LINES","payload":{"frame":{"seq":50,"session_id":"s-LINES","turn_id":"turn-1U5SO1G-4W89TJM6HX","event":{"type":"turn.assistant_message_completed","payload":{"role":"assistant","message":{"role":"assistant","content":"pong"}},"metadata":{"event_id":"event-1U5SO1H-CULFDF3HVM","log_position":50,"turn_id":"turn-1U5SO1G-4W89TJM6HX","command_id":"command-1U5SO1G-D5YKUGNF62","occurred_at":"2026-10-09T02:53:41.399Z"}}},"type":"session_stream_event"},"cursor":50,"durable":true}
← {"sequence":8,"session_id":"s-LINES","payload":{"frame":{"seq":51,"session_id":"s-LINES","turn_id":"turn-1U5SO1G-4W89TJM6HX","event":{"type":"turn.usage","payload":{"input-tokens":0,"output-tokens":8,"cached-input-tokens":940,"total-tokens":948,"cost_known":false,"cost_usd":0.0,"finish-reason":"stop …186 characters… t-1U5SO1H-YIUWCGCZ8E","log_position":51,"turn_id":"turn-1U5SO1G-4W89TJM6HX","command_id":"command-1U5SO1G-D5YKUGNF62","occurred_at":"2026-10-09T02:53:41.401Z"}}},"type":"session_stream_event"},"cursor":51,"durable":true}
← {"sequence":9,"session_id":"s-LINES","payload":{"frame":{"seq":52,"session_id":"s-LINES","turn_id":"turn-1U5SO1G-4W89TJM6HX","event":{"type":"turn.completed","payload":{"turn-number":1,"message-count":2},"metadata":{"event_id":"event-1U5SO1H-JYVW3AHNT","log_position":52,"turn_id":"turn-1U5SO1G-4W89TJM6HX","command_id":"command-1U5SO1G-D5YKUGNF62","occurred_at":"2026-10-09T02:53:41.404Z"}}},"type":"session_stream_event"},"cursor":52,"durable":true}
· stdin closed
← {"sequence":0,"payload":{"sessions":[],"type":"organism.activity"}}
(exit 0)
```

Three wrong lines, three refusals, and the fourth line ran.

**Mistake:**

```text
$ nodecode rpc s-CANCEL
  (15 more lines)
→ {"type": "cancel_turn", "request": {"turn_id": "turn-1U5SO2H-ROB7B7VNHO"}}
← … "session_id":"s-CANCEL","command":"cancel_turn","status":"rejected","message":"session_id and turn_id are required","code":"invalid_request"}},"session_id":"s-CANCEL"}
← {"sequence":9,"session_id":"s-CANCEL","payload":{"turn_id":"turn-1U5SO2H-ROB7B7VNHO","delta":{"type":"text","text":"a l"},"type":"item_delta"}}
  (9 more lines)
(exit 0)
```

**Instead:**

```text
$ nodecode rpc s-CANCEL
  (20 more lines)
→ {"type": "cancel_turn", "turn_id": "turn-1U5SO2H-ROB7B7VNHO"}
← … "session_id":"s-CANCEL","command":"cancel_turn","status":"accepted","message":"cancellation requested","response":{"turn_id":"turn-1U5SO2H-ROB7B7VNHO" …
  (2 more lines)
← … "turn_id":"turn-1U5SO2H-ROB7B7VNHO","event":{"type":"turn.cancelled","payload":{"reason":"stopped by the operator"} …
  (2 more lines)
(exit 0)
```

`cancel_turn` takes `turn_id` beside `type`. Under `request` it is refused, and the turn keeps streaming.

**Mistake:**

```text
$ nodecode rpc s-QUEUE
  (9 more lines)
→ {"type":"start_turn","request":{"prompt":"say pong"}}
  (2 more lines)
← … "session_id":"s-QUEUE","command":"start_turn","status":"queued","message":"turn input queued","response":{"turn_id":"turn-1U5SO1H-NFFNSP0HIP" …
  (19 more lines)
(exit 0)
```

**Instead:**

```text
$ nodecode rpc s-STEER
  (14 more lines)
→ {"type": "steer_turn", "request": {"prompt": "say pong", "expected_turn_id": "turn-1U5SO1J-SQ15L3N450"}}
  (2 more lines)
← … "session_id":"s-STEER","command":"steer_turn","status":"accepted","message":"turn steered","response":{"turn_id":"turn-1U5SO1J-SQ15L3N450" …
  (16 more lines)
(exit 0)
```

A second `start_turn` waits behind the first. `steer_turn` reaches the running turn at its next round boundary.

Mistakes to avoid:

- Writing the prompt as text. `invalid JSON: malformed JSON at byte 0: #\s begins no value` Instead: `{"type":"start_turn","request":{"prompt":"say pong"}}`
- Putting `prompt` beside `type`. `request must be an object` Instead: The prompt goes under `request`.
- Leaving out `type`. `type must be a non-empty string` Instead: Every line names its `type`.
- Steering without `expected_turn_id`. Rejected: `session_id, expected_turn_id, and non-empty prompt are required` Instead: Send the `turn_id` from your `start_turn` result.

## Lines out

Each line is one frame, exactly as the gateway sent it. A turn ends with turn.completed, `turn.failed` or `turn.cancelled`.

| Frame | Is |
| --- | --- |
| session_stream_resumed | Always first. The session's replay, in `accumulated_frames`. |
| session_stream_event | A fact as it lands. It carries `"durable":true` and a `cursor`, its place in the log. |
| item_delta | Text or a tool call while it streams. No cursor. |
| gateway_turn_command_result | A command's answer: `accepted`, `queued` or `rejected`, with the `turn_id`. |
| organism.activity | The organism's own frames. `sequence` 0 and no session. |
| `sync_protocol_error` | A line that is not a command. |

```text
$ nodecode rpc s-DOCS
→ {"type": "start_turn", "request": {"prompt": "how many lisp files"}}
← {"sequence":2,"session_id":"s-DOCS","payload":{"type":"session_stream_resumed","session_id":"s-DOCS","accumulated_frames":[ …6 replayed frames… ],"admission_state":"ready","replay_chunk_index":0,"replay_chunk_final":true},"cursor":22}
← {"sequence":0,"payload":{"sessions":[],"type":"organism.activity"}}
← {"sequence":3,"session_id":"s-DOCS","payload":{"frame":{"seq":24,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.input_committed","payload":{"role":"user","message":"how many lisp files","prompt-id":"command-1U5SO1A-INEOFW90L2","sequence":1,"disposition":"started"},"metadata":{"event_id":"event-1U5SO1A-3Y4MN9JLPB","log_position":24,"turn_id":"turn-1U5SO1A-E37X7WSI6","command_id":"command-1U5SO1A-INEOFW90L2","occurred_at":"2026-10-09T02:53:34.915Z"}}},"type":"session_stream_event"},"cursor":24,"durable":true}
← {"sequence":4,"session_id":"s-DOCS","payload":{"frame":{"seq":25,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.started","payload":{"input":"how many lisp files","parent-turn-id":"turn-1U5SO1A-2HJ0FRDIE2"},"metadata":{"event_id":"event-1U5SO1A-D49127X4RP","log_position":25,"turn_id":"turn-1U5SO1A-E37X7WSI6","command_id":"command-1U5SO1A-INEOFW90L2","occurred_at":"2026-10-09T02:53:34.916Z"}}},"type":"session_stream_event"},"cursor":25,"durable":true}
← {"sequence":0,"payload":{"sessions":[{"id":"s-DOCS","started_at":"2026-10-09T02:53:34.916Z"}],"type":"organism.activity"}}
← {"sequence":0,"payload":{"type":"gateway_turn_command_result","result":{"command_id":"command-1U5SO1A-INEOFW90L2","correlation_id":"command-1U5SO1A-INEOFW90L2","session_id":"s-DOCS","command":"start_turn","status":"accepted","message":"turn admitted","response":{"turn_id":"turn-1U5SO1A-E37X7WSI6","queue_depth":0},"message_id":"command-1U5SO1A-INEOFW90L2","sequence":1}},"session_id":"s-DOCS"}
← {"sequence":0,"payload":{"type":"commands_changed"}}
← {"sequence":0,"payload":{"text":"ok","type":"organism.note"}}
← {"sequence":5,"session_id":"s-DOCS","payload":{"turn_id":"turn-1U5SO1A-E37X7WSI6","delta":{"type":"tool_call","index":0,"name":"eval","arguments":""},"type":"item_delta"}}
← {"sequence":6,"session_id":"s-DOCS","payload":{"turn_id":"turn-1U5SO1A-E37X7WSI6","delta":{"type":"tool_call","index":0,"name":"eval","arguments":"{\"for"},"type":"item_delta"}}
  +9 more item_delta frames
← {"sequence":16,"session_id":"s-DOCS","payload":{"frame":{"seq":28,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.assistant_message_completed","payload":{"role":"assistant","message":{"role":"assistant","content":null,"tool_calls":[{"id":"call_ec27431c907f415a8c40","ty …146 characters… nt-1U5SO1B-2G2BBRJOZP","log_position":28,"turn_id":"turn-1U5SO1A-E37X7WSI6","command_id":"command-1U5SO1A-INEOFW90L2","occurred_at":"2026-10-09T02:53:35.959Z"}}},"type":"session_stream_event"},"cursor":28,"durable":true}
← {"sequence":17,"session_id":"s-DOCS","payload":{"frame":{"seq":29,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.usage","payload":{"input-tokens":740,"output-tokens":14,"cached-input-tokens":226,"total-tokens":980,"cost_known":false,"cost_usd":0.0,"finish-reason":"too …192 characters… nt-1U5SO1B-X4RWS89GB3","log_position":29,"turn_id":"turn-1U5SO1A-E37X7WSI6","command_id":"command-1U5SO1A-INEOFW90L2","occurred_at":"2026-10-09T02:53:35.960Z"}}},"type":"session_stream_event"},"cursor":29,"durable":true}
← {"sequence":18,"session_id":"s-DOCS","payload":{"frame":{"seq":30,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.tool_call_started","payload":{"call-id":"call_ec27431c907f415a8c40","tool-name":"eval","arguments":"{\"form\": \"(length (directory \\\"src/**/*.lisp\\\"))\"}"},"metadata":{"event_id":"event-1U5SO1B-Y4B15O9AQZ","log_position":30,"turn_id":"turn-1U5SO1A-E37X7WSI6","command_id":"command-1U5SO1A-INEOFW90L2","occurred_at":"2026-10-09T02:53:35.962Z"}}},"type":"session_stream_event"},"cursor":30,"durable":true}
← {"sequence":19,"session_id":"s-DOCS","payload":{"frame":{"seq":31,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.tool_result","payload":{"call-id":"call_ec27431c907f415a8c40","tool-name":"eval","result":"3","duration-ms":37,"failed":false},"metadata":{"event_id":"event-1U5SO1C-8NX8NVLEJ6","log_position":31,"turn_id":"turn-1U5SO1A-E37X7WSI6","command_id":"command-1U5SO1A-INEOFW90L2","occurred_at":"2026-10-09T02:53:36.007Z"}}},"type":"session_stream_event"},"cursor":31,"durable":true}
← {"sequence":20,"session_id":"s-DOCS","payload":{"session_id":"s-DOCS","process":{"shell_session_id":1000,"tool_call_id":"call_ec27431c907f415a8c40","command":"(length (directory \"src/**/*.lisp\"))","status":"exited","output_b64":"","offset":0,"exit_code":0},"type":"session_background_process_updated"}}
← {"sequence":21,"session_id":"s-DOCS","payload":{"turn_id":"turn-1U5SO1A-E37X7WSI6","delta":{"type":"text","text":"The"},"type":"item_delta"}}
← {"sequence":22,"session_id":"s-DOCS","payload":{"turn_id":"turn-1U5SO1A-E37X7WSI6","delta":{"type":"text","text":"re "},"type":"item_delta"}}
  +10 more item_delta frames
← {"sequence":33,"session_id":"s-DOCS","payload":{"frame":{"seq":34,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.assistant_message_completed","payload":{"role":"assistant","message":{"role":"assistant","content":"There are 3 Lisp files under src/."}},"metadata":{"event_id":"event-1U5SO1C-SRRGVGMD65","log_position":34,"turn_id":"turn-1U5SO1A-E37X7WSI6","command_id":"command-1U5SO1A-INEOFW90L2","occurred_at":"2026-10-09T02:53:36.568Z"}}},"type":"session_stream_event"},"cursor":34,"durable":true}
← {"sequence":34,"session_id":"s-DOCS","payload":{"frame":{"seq":35,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.usage","payload":{"input-tokens":87,"output-tokens":10,"cached-input-tokens":966,"total-tokens":1063,"cost_known":false,"cost_usd":0.0,"finish-reason":"sto …186 characters… nt-1U5SO1C-FSPCTWEG8I","log_position":35,"turn_id":"turn-1U5SO1A-E37X7WSI6","command_id":"command-1U5SO1A-INEOFW90L2","occurred_at":"2026-10-09T02:53:36.568Z"}}},"type":"session_stream_event"},"cursor":35,"durable":true}
← {"sequence":35,"session_id":"s-DOCS","payload":{"frame":{"seq":36,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.completed","payload":{"turn-number":2,"message-count":6},"metadata":{"event_id":"event-1U5SO1C-9KUUK9H47E","log_position":36,"turn_id":"turn-1U5SO1A-E37X7WSI6","command_id":"command-1U5SO1A-INEOFW90L2","occurred_at":"2026-10-09T02:53:36.569Z"}}},"type":"session_stream_event"},"cursor":36,"durable":true}
· stdin closed
← {"sequence":0,"payload":{"sessions":[],"type":"organism.activity"}}
(exit 0)
```

A second turn on a named session, with one evaluation. The replay holds the first turn, its `turn.completed` included.

**Mistake:**

```text
$ nodecode rpc s-GREP
→ {"type":"start_turn","request":{"prompt":"say pong"}}
← … "turn_id":"turn-1U5SO2J-4SA7KWPKBW","event":{"type":"turn.completed","payload":{"turn-number":1,"message-count":2} …
· stdin closed
(exit 0)
```

**Instead:**

```text
$ nodecode rpc s-DOCS
→ {"type": "start_turn", "request": {"prompt": "how many lisp files"}}
  (5 more lines)
← … "status":"accepted","message":"turn admitted","response":{"turn_id":"turn-1U5SO1A-E37X7WSI6","queue_depth":0},"message_id":"command-1U5SO1A-INEOFW90L2" …
  (15 more lines)
← … "payload":{"frame":{"seq":36,"session_id":"s-DOCS","turn_id":"turn-1U5SO1A-E37X7WSI6","event":{"type":"turn.completed","payload":{"turn-number":2 …
  (2 more lines)
(exit 0)
```

The replay carries the last turn's `turn.completed`. Wait for the end whose `turn_id` is the one your `start_turn` result carried.

Mistakes to avoid:

- Taking `accepted` for the answer. `turn admitted` came at 0.11 s. The answer's first word came at 1.50 s. Instead: Read on to the turn's own `turn.completed`.
- Keeping `item_delta` text as the answer. Deltas are not facts. They have no cursor, and a resume does not send them again. Instead: The answer is in `turn.assistant_message_completed`.
- Reading `sequence` as a place in the log. `sequence` counts the frames sent to this client, and it is 0 on the organism's own frames. Instead: The place in the log is `cursor`.

## Ending

| When | rpc | The organism |
| --- | --- | --- |
| stdin ends | Exits 0 | Stops if rpc started it, and a running turn stops with it. Keeps running if it was already serving. |
| The gateway ends | Exits 1, why on stderr | Gone. |
| A bad argument | Exits 2 | Never started. |

**Mistake:**

```text
$ echo '…' | nodecode rpc s-ECHO
→ {"type":"start_turn","request":{"prompt":"say pong"}}
· stdin closed
(exit 0)
```

**Instead:**

```text
$ nodecode rpc
→ {"type":"start_turn","request":{"prompt":"say pong"}}
  (6 more lines)
← … "session_id":"s-GW3YSGSZ","command":"start_turn","status":"accepted","message":"turn admitted","response":{"turn_id":"turn-1U5SO19-KHY4SY503B" …
  (4 more lines)
← … "turn_id":"turn-1U5SO19-KHY4SY503B","event":{"type":"turn.completed","payload":{"turn-number":1,"message-count":2} …
· stdin closed
  (1 more line)
(exit 0)
```

Closing stdin stops the organism rpc started, and the turn with it. The next start picks the turn up. Close stdin after your turn's end.

```text
$ nodecode rpc s-SERVED
← {"sequence":2,"session_id":"s-SERVED","payload":{"type":"session_stream_resumed","session_id":"s-SERVED","accumulated_frames":[ …6 replayed frames… ],"admission_state":"ready","replay_chunk_index":0,"replay_chunk_final":true},"cursor":159}
← {"sequence":0,"payload":{"text":"ok","type":"organism.note"}}
← {"sequence":0,"payload":{"sessions":[],"type":"organism.activity"}}
· kill -TERM the serve
← {"sequence":0,"payload":{"type":"commands_changed"}}
! nodecode: the gateway closed the connection
(exit 1)
```

Attached to a `nodecode serve`, which was then stopped.

Mistakes to avoid:

- Reading exit 0 as "the turn finished". Exit 0 means stdin ended. It says nothing about any turn. Instead: The turn's end is a frame: `turn.completed`, `turn.failed` or `turn.cancelled`.
- Expecting rpc's exit to stop a `nodecode serve` it attached to. An organism that was already serving keeps running, and so does its turn. Instead: Stop it where you started it, or with `nodecode gateway off`.

## Resume

Name the session and the newest cursor you read. The replay then carries only what landed past it, and says resumed_after.

```sh
nodecode rpc s-DOCS --cursor 22
```

```text
$ nodecode rpc s-DOCS --cursor 22
← {"sequence":3,"session_id":"s-DOCS","payload":{"type":"session_stream_resumed","session_id":"s-DOCS","accumulated_frames":[ …9 replayed frames… ],"admission_state":"ready","replay_chunk_index":0,"replay_chunk_final":true,"resumed_after":22},"cursor":36}
← {"sequence":0,"payload":{"sessions":[],"type":"organism.activity"}}
← {"sequence":0,"payload":{"type":"commands_changed"}}
→ {"type":"get_state"}
← {"sequence":0,"payload":{"type":"gateway_turn_command_result","result":{"command_id":"command-1U5SO1D-9721LB40BB","correlation_id":"command-1U5SO1D-9721LB40BB","session_id":"s-DOCS","command":"get_state","status":"accepted","message":"session state","response":{"queue_depth":0,"session":{"session_id …287 characters… axolotl-cyber"},"pin":{"provider":"nodecode","model":"nodecode-axolotl-cyber"},"effort":"max","efforts":["off","minimal","low","medium","high","xhigh","max"],"refusal":null},"pending_prompts":[]}}},"session_id":"s-DOCS"}
· stdin closed
(exit 0)
```

Nine facts past 22: the second turn alone. Without `--cursor`, the same session replays all fifteen.

| You run | You get |
| --- | --- |
| `rpc SESSION-ID --cursor N` | `resumed_after` N and the facts past N. Add them to what you hold. |
| `rpc SESSION-ID` | The whole replay, no `resumed_after`. Replace what you hold. |
| `--cursor` past the log, or the head moved since | The whole replay, no `resumed_after`. |
| `rpc` | A fresh session and an empty replay. |

**Mistake:**

```text
$ nodecode rpc s-DOCS --cursor 9
← {"sequence":4,"session_id":"s-DOCS","payload":{"type":"session_stream_resumed","session_id":"s-DOCS","accumulated_frames":[ …15 replayed frames… ],"admission_state":"ready","replay_chunk_index":0,"replay_chunk_final":true,"resumed_after":9},"cursor":36}
  (3 more lines)
(exit 0)
```

**Instead:**

```text
$ nodecode rpc s-DOCS --cursor 22
← {"sequence":3,"session_id":"s-DOCS","payload":{"type":"session_stream_resumed","session_id":"s-DOCS","accumulated_frames":[ …9 replayed frames… ],"admission_state":"ready","replay_chunk_index":0,"replay_chunk_final":true,"resumed_after":22},"cursor":36}
  (5 more lines)
(exit 0)
```

`--cursor` takes a frame's `cursor`. A `sequence` number resumes from far back and sends what you already hold.

**Mistake:**

```text
$ nodecode rpc
← {"sequence":1,"session_id":"s-ANCDJG21","payload":{"type":"session_stream_resumed","session_id":"s-ANCDJG21","accumulated_frames":[] …
  (2 more lines)
(exit 0)
```

**Instead:**

```text
$ nodecode rpc s-DOCS
← {"sequence":5,"session_id":"s-DOCS","payload":{"type":"session_stream_resumed","session_id":"s-DOCS","accumulated_frames":[ …15 replayed frames… ],"admission_state":"ready","replay_chunk_index":0,"replay_chunk_final":true},"cursor":36}
  (3 more lines)
(exit 0)
```

With no session id you get a new, empty session. Pass the `session_id` from the first frame.

Mistakes to avoid:

- Adding every replay to what you hold. With no `resumed_after`, the replay is whole: `--cursor 9999` got all fifteen facts. Instead: Replace your copy when `resumed_after` is absent.
- `nodecode rpc --cursor 5`. Exit 2: `--cursor resumes a session past a log position: nodecode rpc SESSION-ID --cursor N` Instead: Name the session first.
- `--cursor five`. Exit 2: `--cursor takes a log position, not "five"` Instead: A whole number, from a frame's `cursor`.

## get_state

`{"type":"get_state"}` answers with the session as it stands. It needs no subscription.

```text
{
  "queue_depth": 0,
  "session": {
    "session_id": "s-DOCS",
    "cwd": "/home/you/project",
    "parent": false,
    "created_at": "2026-10-09T02:53:34.076Z"
  },
  "title": "say pong",
  "admission_state": "ready",
  "cursor": 36,
  "target": {
    "provider": "nodecode",
    "model": "nodecode-axolotl-cyber",
    "unconfigured": false,
    "default": {
      "provider": "nodecode",
      "model": "nodecode-axolotl-cyber"
    },
    "pin": {
      "provider": "nodecode",
      "model": "nodecode-axolotl-cyber"
    },
    "effort": "max",
    "efforts": [
      "off",
      "minimal",
      "low",
      "medium",
      "high",
      "xhigh",
      "max"
    ],
    "refusal": null
  },
  "pending_prompts": []
}
```

| Field | Is |
| --- | --- |
| `turn_id` | The running turn. Absent when none runs. |
| `queue_depth` | Prompts waiting behind it. |
| `admission_state` | `ready` or `active`. |
| `cursor` | The newest place in the log: the one to resume from. |
| `target` | The provider, model and effort the next turn uses. |
| `pending_prompts` | The queued prompts. |

## The schema

Every command and every frame, both ways, is `src/src/wire/protocol.schema.json`, JSON Schema 2020-12. Of the 596 JSON lines the runs behind this page sent and got back, 591 check against it. The other five are wrong commands sent on purpose. Check yours before you send them.

See [Troubleshooting](https://nodecode.ai/docs/troubleshooting#rpc-failures) for a line you got, and [The gateway](https://nodecode.ai/docs/gateway) for keeping an organism running.
