rho-coding-agent 2.19.0

A fast Rust agent harness with a small footprint and opinionated defaults
# Cursor ACP spike recordings

Recorded 2026-10-05 against `cursor-agent 2026.10.01-e373342` (`cursor-agent --trust
--model composer-2.5 acp`), API-key auth (`CURSOR_API_KEY`), Linux x86_64. A Python
ACP client logged every wire message as `{"dir", "ms", "msg"}` rows: `c2a` is
client→agent, `a2c` is agent→client, `ms` is monotonic milliseconds since spawn,
and the last row is a timing summary. To keep the files small, model lists and
`available_commands_update` payloads are trimmed, workspace paths are rewritten
to `/workspace`, and the cancel recording keeps only the first 12 chunks.

| File | Scenario |
|---|---|
| `cursor_agent_tools.jsonl` | Agent mode, user `approvalMode: unrestricted`: read, grep, shell, two edits. No permission requests |
| `cursor_permission_reject.jsonl` | `approvalMode: allowlist`, empty allow list, client answers `reject-once`: shell asks, read and edit do not |
| `cursor_plan_mode.jsonl` | `session/set_mode plan` + allowlist + reject: edit blocked by mode, shell asks then blocked |
| `cursor_cancel.jsonl` | `session/cancel` mid-turn |
| `cursor_deny_read_fetch.jsonl` | `allowlist` + deny `Read(**)`, `WebFetch(*)`: read is denied ("Permission denied", reported `completed`); grep and glob still run; fetch **ignores** the deny and asks via `session/request_permission` (`kind: fetch`, `toolCallId: web_fetch_0`) |
| `cursor_ext_update_todos.jsonl` | `cursor/update_todos` arrives as a JSON-RPC request with no `sessionId` |

## Checklist answers

1. **Flags before `acp`.** `--model` is honored (`session/new` reports the
   `currentModelId`; `composer-2.5` resolved to `composer-2.5[fast=true]`).
   `--mode plan` is ignored: `currentModeId` stayed `agent` and every tool ran.
   `--allowed-tools read_tool_call` is ignored: shell and edit both ran. Plan
   mode has to go through `session/set_mode` (answers `{}` and emits
   `current_mode_update`). Cursor also implements `session/set_config_option`
   and `unstable_setSessionModel`.
2. **Permission requests depend on the user's Cursor config, not on ACP.** With
   the default `approvalMode: unrestricted` (Run Everything), nothing asks.
   With `allowlist`, only shell asks (`kind: execute`); reads and file edits
   never ask, even with an empty allow list. Plan mode (via `set_mode`) blocks
   edits but **not** shell: under `unrestricted`, plan mode ran
   `echo > shell.txt`. Cursor's `permissions.deny` (`Write(**)`, `Shell(*)`)
   does fence (`Read(**)` too); `WebFetch(*)` does **not**, but under
   `allowlist` fetch asks permission, so rejection fences it. Grep and glob are
   not covered by `Read(**)`. Denied and rejected tools then report `status: completed` with no
   output, not `failed`. `allow-always` persists into Cursor's allowlist
   (source), so Rho must never pick it. The config dir is
   `$CURSOR_CONFIG_DIR`, else `$XDG_CONFIG_HOME/cursor`, else `~/.cursor`, and
   it holds `cli-config.json`.
   - Tool shapes: `tool_call` carries `kind` (`read`/`search`/`execute`/`edit`/`other`),
     `title`, `rawInput` (`{path}`, `{command}`, `{pattern,path}`), `locations`.
     Edit and search calls start with an empty `rawInput`, which a
     `tool_call_update` fills in. Results: read → `rawOutput.content`; shell →
     `rawOutput.{exitCode,stdout,stderr}`; grep → `rawOutput.{totalMatches,truncated}`;
     edit → `content: [{type: diff, path, oldText, newText}]`. A new-file diff
     uses `oldText: "-- /dev/null"` and prefixes `newText` with a `++ b/<path>`
     header line. Todos show up as `kind: other`, `rawInput._toolName: updateTodos`.
3. **Auth.** `authenticate(cursor_login)` errors immediately (`-32602`, "Failed
   to open browser for login", plus a login URL) when only an API key is
   present. `session/new` works without `authenticate`, and when
   unauthenticated it returns an `authRequired` error rather than hanging
   (source). So Rho should not call `authenticate`. `cursor-agent status
   --format json` reports `unauthenticated` even when `CURSOR_API_KEY` works.
4. **Session ids.** `cursor-agent --resume <acp sessionId> -p …` did not see
   the ACP conversation. The ACP `sessionId` is not a `--resume` chat id.
5. **Extensions.** `cursor/update_todos`, `cursor/task` and `cursor/generate_image`
   are sent as JSON-RPC **requests** (with an `id`, no `sessionId`), and the
   agent ignores errors on them (source: `sendNonBlockingExtensionNotification`
   → `extMethod(...).catch`). `cursor/ask_question` and `cursor/create_plan` are
   blocking `extMethod` requests. In the source, `ask_question` can also come in
   as `session/request_permission` with one `allow_once` option per answer and
   a `__ask_question_skip__` `reject_once` option. The live run never triggered
   `ask_question`. Undocumented extras in the source: `cursor/canvas`,
   `cursor/list_available_models` (client→agent), and non-standard
   `subagent_spawned` / `subagent_state_update` session updates, sent only when
   the client advertises a subagents capability.
6. **Cancel and exit.** `session/cancel` produced `stopReason: cancelled` 4 ms
   later. After a prompt turn, the process **never** exited on stdin EOF within
   10 s (10/10 runs). Idle sessions exited in about 564 ms (3/4). Rho must
   kill the child after the turn.
7. **Usage.** No `usage_update` in any recording (`sessionUpdate` kinds seen:
   `agent_message_chunk`, `agent_thought_chunk`, `tool_call`, `tool_call_update`,
   `available_commands_update`, `session_info_update`, `current_mode_update`).
   The source never emits `usage_update`. Token and cost totals regress
   compared with `-p`.
8. **Handshake latency** (n=14): `initialize` 420–503 ms; `session/new`
   2411–3381 ms. A 10 s per-step bound is about 3x the worst observed.