acpx-faq ยท diff
v0.4.0 to v0.5.0
242 added, 350 removed. Audit A to A.
---
name: acpx-faq
- description: Run coding agents (codex, claude, Antigravity/agy) headlessly through the acpx ACP CLI. Use before launching or prompting an acpx subagent, and when an acpx command fails, a session is not found, or a prompt seems lost.
+ description: Run coding agents (codex, claude, agy/Antigravity) through the acpx ACP CLI - the headless lane outside a herdr pane (no HERDR_ENV). Use before launching or prompting a subagent, and when a command fails, a session is not found, or a prompt is lost.
metadata:
- version: "0.4.0"
+ version: "0.5.0"
categories: "agents, operations"
topics: "acpx, acp, agent-orchestration, troubleshooting, headless-agents"
upstream: "acpx@0.15.1, agy@1.1.28, agy_acp_server@20260818_01_RC01"
openclaw:
homepage: https://github.com/tenequm/skills/tree/main/skills/acpx-faq
emoji: "๐"
---
# acpx FAQ
Drive coding agents headlessly through [acpx](https://github.com/openclaw/acpx) (>= 0.15.1).
- Command syntax: `acpx --skill show acpx` - the binary ships its own reference and is the
- tiebreaker on syntax. That doc is strong on command shape and silent on failure semantics: it
- has no exit-code table, no `status` state table, and does not mention `--mcp-config` at all.
- This file is the other half.
- Read the Invariants, then your agent's section in **Per agent** - each is a complete
- launch-to-result recipe. After that: Sessions, Completion, MCP, Limits, Failures.
+ ## Syntax authority
- ## Invariants
+ Syntax comes from the binary, not this file. `acpx --skill show acpx` is the vendor
+ reference (~29k chars) and the tiebreaker on command shape - do NOT dump it into the
+ transcript. Retrieve in this order:
- 1. **Global flags precede the agent subcommand.** `--cwd`, `--model`, `--approve-all`,
- `--agent`, `--format`, `--timeout`, `--ttl`, `--mcp-config` are all global. The subcommand
- accepts only `-s`, `--no-wait`, `-f` (and `exec` also takes `--config-option`). Putting a
- global flag after the agent exits **2** with `error: unknown option '--cwd'`.
- 2. **`-s <name>` never creates a session.** It resolves one, walking from `--cwd` up to the
- git root. No match exits **4**. Create first with `sessions ensure --name <n>`.
- 3. **`status` is not a turn signal.** It reports the queue-owner process, not the turn, and
- keeps saying `running` after the turn has ended. Never gate automation on it.
- 4. **Permission flags only gate requests the adapter chooses to raise.** The claude adapter
- spawns its binary with `--allow-dangerously-skip-permissions --setting-sources=project,local`,
- so it never raises an ACP permission request and there is nothing for acpx to deny; codex
- writes through its terminal capability. `--deny-all` is not ignored, it is unreachable.
- Isolation comes from the `--cwd` you hand it, never from a flag.
- 5. **`[done] end_turn` and exit 0 are not proof of success.** A content-filter kill, an MCP
- load failure, or a truncated turn all end that way. Read the stream, or require the agent
- to write a result file you can check.
- 6. **A persistent session leaves a resident process stack until you close it.** Every `-s`
- prompt elects a queue owner holding `npm exec -> node <adapter> -> <agent binary>`,
- roughly 700 MB per executor. `sessions close` is the only supported teardown; the idle
- TTL (default 300s) is the backstop, and `--ttl 0` removes it. See Teardown.
+ 1. `acpx --help` / `acpx <cmd> --help` - the flags you are about to compose.
+ 2. One topic: `acpx --skill show acpx | rg -B2 -A12 '<term>'`, or a section:
+ `acpx --skill show acpx | sed -n '/^## Sessions/,/^## /p'`.
+ 3. Authoring something novel (a flow, an embedded runtime): dump ONCE to a scratch
+ file and grep that file.
- ## Per agent
+ This file repeats nothing the reference covers. It carries only what the reference
+ lacks: exit codes, failure semantics, traps, and per-agent recipes. If a syntax
+ question is not answered here, it is answered there - go get it, never guess a flag.
- Each section is self-contained: flags, launch, and the quirks that bite while it runs.
+ ## When this lane
- **Default to `exec` for dispatched work.** `exec` runs a temporary session: no saved record,
- and not queue-aware, so it never elects a queue owner and leaves nothing to clean up. Reach
- for `-s <name>` only when you will queue follow-ups onto the live run or read
- `sessions history` afterwards - and then the closing `sessions close` is part of the recipe,
- not an optional courtesy (invariant 6).
+ acpx is the headless out-of-process executor lane. Inside a herdr pane (`HERDR_ENV=1`)
+ prefer herdr (herdr-faq): it gives you screen reads and dialog detection. Everywhere
+ else (plain terminal, ssh, cron) herdr is untargetable and acpx is the lane.
+ In-session fan-out stays on the harness's own subagent tool.
- ### agy / Antigravity - the `--agent` escape hatch
+ ## Invariants
- **acpx has no Antigravity adapter and never will get one.** Issue #362 was closed as
- externally blocked: Antigravity ships no supported ACP stdio mode. The built-in `gemini` agent
- is the *public Gemini CLI*, a different product, and it is dead for Code Assist:
+ 1. **Global flags precede the agent subcommand** (`--cwd`, `--model`, `--approve-all`,
+ `--agent`, `--format`, `--timeout`, `--ttl`, `--mcp-config`). The subcommand accepts
+ only `-s`, `--no-wait`, `-f` (and `exec` also `--config-option`). A global flag
+ after the agent exits **2** with `error: unknown option`.
+ 2. **`-s <name>` never creates a session.** It resolves one, walking from `--cwd` up
+ to the git root; no match exits **4**. Create first with `sessions ensure --name <n>`.
+ 3. **`status` is not a turn signal.** It probes the queue-owner process, never the
+ agent, and keeps saying `running` after the turn has ended. Never gate automation
+ on it.
+ 4. **Permission flags are not a sandbox.** The claude adapter spawns its binary with
+ `--allow-dangerously-skip-permissions --setting-sources=project,local`, so no
+ permission request ever reaches acpx's policy layer (`--deny-all` is unreachable,
+ not ignored); codex writes through its terminal capability. `--no-terminal`
+ genuinely removes the terminal capability, but the only real filesystem control is
+ which `--cwd` you hand it. Verify: `ps -Ao args | grep claude-agent-sdk` mid-turn.
+ 5. **`[done] end_turn` and exit 0 are not proof of success.** A content-filter kill,
+ an MCP load failure, or a truncated turn all end that way. Read the stream, or
+ require the agent to write a result file you can check.
+ 6. **A persistent session leaves a resident process stack until you close it.** Every
+ `-s` prompt elects a queue owner holding `npm exec -> node <adapter> -> <agent>`,
+ roughly 700 MB per executor. `sessions close` is the only supported teardown; the
+ idle TTL (default 300s) is the backstop, and `--ttl 0` removes it. See Teardown.
- ```
- [error] RUNTIME: This client is no longer supported for Gemini Code Assist for individuals.
- To continue using Gemini, please migrate to the Antigravity suite of products
- ```
+ ## Per agent
- The only route is Google's own signed ACP server, reached through the raw-command escape
- hatch. **What the `.par` is:** installed by Antigravity under
- `~/.local/lib/antigravity-acp/` and sha512-verified against Google's release manifest. Neither
- acpx nor this skill downloads it - if it is missing, install Antigravity. It is a readable zip
- of a Google-built Python runtime, pinned at build `agy_acp_server_20260818_01_RC01` because it
- self-updates nothing. `localharness_external` ships beside it and must stay executable, or the
- server starts anyway and only logs `Localharness not found`.
+ **Default to `exec` for dispatched work.** `exec` runs a temporary session: no saved
+ record, not queue-aware, so it never elects a queue owner and leaves nothing to clean
+ up. Use `-s <name>` only to queue follow-ups onto a live run or to read
+ `sessions history` afterwards - and then the closing `sessions close` is part of the
+ recipe (invariant 6). One directory (ideally one worktree) per parallel executor:
+ sessions key on cwd.
+ ### agy / Antigravity - the `--agent` escape hatch
+
+ **acpx has no Antigravity adapter and never will** (issue #362, externally blocked: no
+ supported ACP stdio mode). The built-in `gemini` agent is the *public Gemini CLI*, a
+ different product, dead for Code Assist
+ (`[error] RUNTIME: This client is no longer supported for Gemini Code Assist for
+ individuals`). The only route is Google's signed ACP server via `--agent`. The `.par`
+ is installed by Antigravity under `~/.local/lib/antigravity-acp/` (build
+ `agy_acp_server_20260818_01_RC01`, self-updates nothing; if missing, install
+ Antigravity - never download it). `localharness_external` beside it must stay
+ executable, or the server starts anyway and only logs `Localharness not found`.
+
```bash
# macOS: --agent may point straight at the .par
D=/abs/real/dir # must exist: roots resolve through realpath
acpx --agent ~/.local/lib/antigravity-acp/agy_acp_server.par \
--cwd "$D" --model gemini-3.7-flash-medium --timeout 1800 \
exec 'Carry out $D/brief.md. Write your report to $D/report.md.'
```
- **On Linux, `--agent` points at a wrapper, not at the `.par`** - the server needs `--uid=`
- there (every ACP registry entry passes it for `linux-*`, none for `darwin-arm64`), and on NixOS
- a CA bundle too. See Linux / NixOS below.
-
- - **Default to `gemini-3.7-flash-medium`.** Measured on par with Opus for rubric-driven bulk
- work (99.5% verdict agreement across 213 items, and the better call on the one they disputed)
- and far faster; `gemini-3.8-flash-high` scored measurably worse on the same task despite
- being the bigger, higher-effort model. Pick another id only when a task argues for it.
- - **Effort lives in the model id** - `gemini-3.7-flash-{low,medium,high}`,
- `gemini-3.8-flash-high`. This server exposes no separate effort option.
- - **Auth is a settings file, not an env var.** `ACPX_AUTH_OAUTH_PERSONAL=1` is dead in build
- `20260818_01_RC01` - the `.par` carries the string *"Environment-based auth selection has
- been removed."* With no auth type configured the server does not error, it **hangs on
- `authenticate` forever**, which reads exactly like a network stall. Write the type first:
+ - **Default `gemini-3.7-flash-medium`** (measured on par with Opus for rubric-driven
+ bulk work and far faster; `gemini-3.8-flash-high` measured worse on the same task).
+ Effort lives in the model id (`gemini-3.7-flash-{low,medium,high}`); no separate knob.
+ - **agy reads sibling files unprompted** - agents sharing a directory reproduce each
+ other's output. One directory per agent, and checksum any rerun before believing an
+ agreement number.
+ - **Auth is a settings file, not an env var** (`ACPX_AUTH_OAUTH_PERSONAL=1` is dead in
+ this build). With no auth type configured the server **hangs on `authenticate`
+ forever**, which reads like a network stall. Write the type first:
```bash
mkdir -p ~/.gemini/antigravity-acp
echo '{"auth":{"type":"oauth-personal"}}' > ~/.gemini/antigravity-acp/settings.json
```
- Types: `oauth-personal` (the subscription path, the one you want), `gemini-api-key`,
- `agent-platform`. Then sign in once; the token persists and later runs need nothing.
- - **The OAuth browser opens silently; the URL is never printed.** Capture it with a fake
- `xdg-open` (or `open`) early on `PATH` that echoes its argument. The callback port is
- per-run: on a headless box, `curl '<callback-url>'` the redirect your browser could not
- deliver. A session created before auth finished stays broken - start a fresh one.
- - **Never pass a positional agent with `--agent`** - exit **2**,
- `Do not combine positional agent with --agent override`.
- - **This lane is subscription-backed. Leave the auth alone.** On the *CLI* side
- (`~/.gemini/settings.json`, a different file from the ACP server's above),
- `security.auth.selectedType: "oauth-personal"` is the Google subscription path and the
- intended default; there is no tier setting, and `agy` has no auth subcommand at all. If entitlement ever looks wrong it is a server-side lookup with no
- client-side lever - do not "fix" it by switching to `gemini-api-key` or a GCP project, which
- are separate billing arrangements rather than fixes.
- - **Ask an agy verification prompt for a bare reply, and read the whole log.** agy will route
- an answer into a brain artifact file instead of the stream, so a `tail` shows you an
- `AbsolutePath` and nothing else and you cannot tell "no tools" from "answered elsewhere".
- Add "Do not create any files" and capture the full output.
- - **agy hides MCP failures inside a successful-looking turn.** A failed server appears as
- inline text - `MCP load failed for <name>: ... expect initialized request, but received: ...
- "server/discover"` - in a stream that still ends `[done] end_turn` with exit 0. Grep the
- output for `MCP load failed`, do not trust the exit code.
- - **The credential split is total, not just "two onboardings".** The server reads
- `~/.gemini/antigravity-acp/acp_token.json` and never looks at the CLI's
- `~/.gemini/antigravity-cli/antigravity-oauth-token`, so a fully authenticated `agy` does
- nothing for the ACP lane. Its own docstrings give the layout: `<GEMINI_HOME>/antigravity-acp/`
- holds `settings.json`, `acp_token.json`, `conversations/` and `brain/`, while
- `<GEMINI_HOME>/antigravity-cli/skills/` is shared across surfaces. Setting `GEMINI_HOME`
- relocates the whole tree - that is how you run a second isolated lane.
+ Types: `oauth-personal` (the subscription path, the one you want),
+ `gemini-api-key`, `agent-platform`. Sign in once; the token persists.
+ - **The OAuth browser opens silently; the URL is never printed.** Capture it with a
+ fake `xdg-open` (or `open`) early on PATH that echoes its argument; on a headless
+ box `curl` the callback URL yourself. A session created before auth finished stays
+ broken - start fresh.
+ - **Leave the auth alone.** This lane is subscription-backed (`oauth-personal`, also
+ the CLI-side default in `~/.gemini/settings.json` - a different file). An
+ entitlement problem is a server-side lookup with no client lever; do not "fix" it by
+ switching to `gemini-api-key` or a GCP project (separate billing, not fixes).
+ - **Never pass a positional agent with `--agent`** - exit **2**.
+ - **Verification prompts: demand a bare reply and read the whole log.** agy routes
+ answers into brain artifact files, so a `tail` cannot tell "no tools" from
+ "answered elsewhere". Add "Do not create any files".
+ - **agy hides MCP failures inside a successful-looking turn** - grep the output for
+ `MCP load failed`; the exit code stays 0.
+ - **The credential split is total.** The ACP server reads
+ `~/.gemini/antigravity-acp/acp_token.json`, never the CLI's token - a fully
+ authenticated `agy` does nothing for this lane. `GEMINI_HOME` relocates the whole
+ tree; that is how you run a second isolated lane.
#### Linux / NixOS
- Three differences from macOS, all silent.
+ Three silent differences from macOS:
- 1. **`--uid=` is required**, so `--agent` points at a wrapper rather than the `.par`
+ 1. **`--uid=` is required**, so `--agent` points at a wrapper
(`~/.local/bin/agy-acp-server`):
```bash
#!/usr/bin/env bash
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt # NixOS; see 2
exec ~/.local/lib/antigravity-acp/agy_acp_server.par --uid= "$@"
```
- 2. **Without that `SSL_CERT_FILE`, NixOS gets a 502.** The `.par` embeds a Google-built Python
- and OpenSSL whose compiled-in CA paths do not exist there, so every handshake with
- `cloudcode-pa.googleapis.com` fails. Its local proxy (`ccpa_connection/proxy_server.py`)
- swallows that and reports `502 Bad Gateway: Failed to connect to backend API`, and the turn
- still ends `[done] end_turn` with **exit 0** - invariant 5 with a concrete signature. The
- real `CERTIFICATE_VERIFY_FAILED` appears only under `--alsologtostderr`.
- 3. **Install `agy` by hand, not by piping the installer to a shell.** Its last step runs
- `agy install`, which appends PATH blocks to `~/.bashrc` and `~/.profile` - useless when
- `~/.local/bin` is already on PATH via home-manager, and broken when `~/.zshrc` is a
- read-only nix-store symlink. Both binaries link against `/lib64/ld-linux-x86-64.so.2`,
- which resolves through nix-ld; no patchelf needed.
-
- Working invocation:
+ 2. **Without that `SSL_CERT_FILE`, NixOS gets a 502.** The `.par`'s embedded OpenSSL
+ has no valid CA paths there; its local proxy reports
+ `502 Bad Gateway: Failed to connect to backend API` and the turn still ends
+ `[done] end_turn` with **exit 0** (invariant 5). The real
+ `CERTIFICATE_VERIFY_FAILED` appears only under `--alsologtostderr`.
+ 3. **Install `agy` by hand, not via the piped installer** - its `agy install` step
+ writes PATH blocks into `~/.bashrc`/`~/.profile`, broken when rc files are
+ read-only nix-store symlinks. Both binaries resolve through nix-ld; no patchelf.
```bash
acpx --agent ~/.local/bin/agy-acp-server \
--cwd "$D" --model gemini-3.7-flash-medium \
--approve-all --timeout 1800 exec '<prompt>'
```
### codex
```bash
D=/abs/dir
acpx --cwd "$D" codex sessions ensure --name work # idempotent; -s cannot create
- acpx --cwd "$D" codex set model gpt-5.6-sol -s work # -> model set: gpt-5.6-sol
- acpx --cwd "$D" codex set reasoning_effort high -s work # -> config set: ... (5 options)
+ acpx --cwd "$D" codex set model gpt-5.6-sol -s work
+ acpx --cwd "$D" codex set reasoning_effort high -s work
acpx --cwd "$D" --approve-all --timeout 5400 --format quiet --suppress-reads \
codex -s work 'Carry out ./brief.md. Write your report to ./report.md.' > run.log 2>&1 &
# ...after the run finishes and you have read what you need:
acpx --cwd "$D" codex sessions close work # ALWAYS - the run is not over until this returns
```
- For a one-shot, skip the session entirely - `exec --config-option` sets model and effort
- inline (0.14.0+), applied after `--model` and before the prompt:
-
- ```bash
- acpx --cwd "$D" --timeout 1800 codex exec --config-option reasoning_effort=low 'Summarize ./DESIGN.md'
- ```
+ One-shot: `exec --config-option reasoning_effort=low` sets model and effort inline
+ (0.14.0+), applied after `--model` and before the prompt.
- - **The adapter floats.** `@agentclientprotocol/codex-acp@^1.1.5` is a caret on a 1.x, so it
- tracks the latest 1.x on every `npx` resolution. A codex-side change can land without you
- upgrading acpx.
- - **OpenAI's content filter kills benign turns** and the turn ends looking clean
- (`[done] end_turn`, exit 0). Vocabulary like race / sweep / exploit / attack in a filename,
- comment or prompt triggers it. Read the transcript tail for the flag line before believing
- completion. Recover by re-prompting the same `-s <name>` (context survives): rename the
- artifact neutrally and list the remaining steps explicitly.
- - **Queue a follow-up onto a live session** by prompting the same name again - it runs after
- the current turn rather than interrupting. This is how you course-correct a running executor
- without relaunching it.
- - **Headful takeover:** `sessions show <n>` prints a `sessionId` that is the ordinary codex
- rollout id, so `codex resume <sessionId>` opens the same thread in a TUI with model and
- effort intact. **Close first** - `sessions close <n>` - or you get
- `already has an active writer (code -32600)`.
- - `codex queue --thread` does **not** reach an acpx-driven session (see What acpx cannot do).
+ - **The adapter floats** (`@agentclientprotocol/codex-acp@^1.1.5` tracks the latest
+ 1.x on every `npx` resolution) - a codex-side change can land without you upgrading
+ acpx.
+ - **OpenAI's content filter kills benign turns** that end clean (`[done] end_turn`,
+ exit 0). Vocabulary like race / sweep / exploit / attack in a filename, comment or
+ prompt triggers it. Read the transcript tail before believing completion; recover by
+ re-prompting the same `-s <name>` (context survives) with the artifact renamed
+ neutrally and the remaining steps listed explicitly.
+ - **Queue a follow-up onto a live session** by prompting the same name again - it runs
+ after the current turn. This is how you course-correct without relaunching.
+ - **Handing off to a human TUI**: `sessions close` first, then `codex resume
+ <sessionId>` (id from `sessions show` - it is the ordinary codex rollout id).
### claude
```bash
D=/abs/dir
acpx --cwd "$D" --model claude-opus-5 claude sessions ensure -s work
acpx --cwd "$D" --model claude-opus-5 --approve-all --suppress-reads --timeout 2400 \
claude -s work -f /abs/brief.md >> run.log 2>&1 &
# ...after the run finishes and you have read what you need:
acpx --cwd "$D" claude sessions close work # ALWAYS - the run is not over until this returns
```
- - **No effort knob.** `set reasoning_effort` returns `Internal error`. Only the model id.
- - **`--model` and `set model` validate differently, and this surprises people.** The adapter
- advertises `["default","opus[1m]","claude-fable-5[1m]","sonnet","haiku"]`, and `set model`
- refuses anything outside that list:
- `Invalid value for config option model: claude-opus-5 (ACP -32603, adapter reported "Internal error")`.
- The global `--model` at session creation passes the id through to the harness instead, so
- `--model claude-opus-5` works fine. A genuinely unknown id still fails loudly:
+ - **No effort knob** (`set reasoning_effort` returns `Internal error`; expected).
+ - **`--model` and `set model` validate differently.** `set model` refuses anything
+ outside the advertised list
+ (`["default","opus[1m]","claude-fable-5[1m]","sonnet","haiku"]`); the global
+ `--model` at session creation passes the id through to the harness, so
+ `--model claude-opus-5` works. A genuinely unknown id still fails loudly:
`RUNTIME: Internal error: There's an issue with the selected model (<id>)`.
- - **Fable runs**: `--model 'claude-fable-5[1m]'` (quote it - the brackets are shell globs).
- - **User-scope skills are excluded on purpose.** The adapter loads project and local settings
- but not user settings, so a user-level skill or slash command comes back
- `Unknown command: /polish`. Fix with `ACPX_CLAUDE_INCLUDE_USER_SETTINGS=1` - and settings
- bind at **session creation**, so an existing session must be recreated, not re-prompted.
- - **Second account:** export `CLAUDE_CONFIG_DIR` and `CLAUDE_SECURESTORAGE_CONFIG_DIR` before
- `acpx`; the child inherits them. That is the whole mechanism for driving another lane.
- - **The adapter is pinned far behind your CLI.** `claude-agent-acp@^0.60.0` resolves to exactly
- 0.60.0, which pins `@anthropic-ai/claude-agent-sdk@0.3.215`, which bundles claude **2.1.215**
- - regardless of the version on your PATH. Consequences: the bundled binary predates the
- cross-session messaging socket, so these sessions are unreachable by peer messaging, and any
- newer harness feature is simply absent. Override with `--agent` pointed at a newer adapter
- build if you need one.
- - A **session-creation stall** is a known adapter combination bug; acpx's own error text
- recommends `--approve-all` with `nonInteractivePermissions=deny`, upgrading both sides, or
- falling back to `claude exec` as a one-shot.
+ - **Fable runs**: `--model 'claude-fable-5[1m]'` (quote it - the brackets are globs).
+ - **User-scope skills are excluded on purpose** -> `Unknown command: /polish`. Fix
+ with `ACPX_CLAUDE_INCLUDE_USER_SETTINGS=1` - and settings bind at session
+ **creation**, so recreate the session, do not re-prompt it.
+ - **Second account**: export `CLAUDE_CONFIG_DIR` and `CLAUDE_SECURESTORAGE_CONFIG_DIR`
+ before `acpx`; the child inherits them.
+ - **The adapter is pinned far behind your CLI** (`claude-agent-acp@^0.60.0` -> SDK
+ 0.3.215 -> bundled claude 2.1.215, regardless of PATH): no cross-session messaging
+ socket, no newer harness features. Override with `--agent` at a newer adapter build.
+ - A **session-creation stall** is a known adapter bug; acpx's own error text
+ recommends `--approve-all` with `nonInteractivePermissions=deny`, upgrading both
+ sides, or falling back to `exec`.
## Sessions
- A session is keyed on **(agent command, absolute cwd, optional name)**. Because `cwd` is part
- of the key, parallel executors each need their own directory - give every one its own git
- worktree and their per-session model and effort settings cannot race.
-
- | verb | behavior |
- |---|---|
- | `sessions ensure --name <n>` | returns the existing session or creates one - idempotent, safe before every prompt |
- | `sessions new --name <n>` | soft-closes any current session and creates a fresh one (`(replaced <id>)`) |
- | a bare prompt with `-s <n>` | never auto-creates; exits **4** with `Create one: ...` |
- | `sessions show <n>` | `lastActivity`, `lastPrompt`, `historyEntries`, `sessionId`, `closed` |
- | `sessions history <n> --limit N` | the actual turn content |
- | `sessions close <n>` | the teardown verb: marks the record closed, sends ACP `session/close`, and takes down the owner and adapter processes (see Teardown); also required before `codex resume` and `sessions export` |
- | `sessions list --local` | local records including closed ones |
- | `sessions prune` | deletes closed records - they persist indefinitely otherwise |
-
- Resolution without `-s` walks from `--cwd` up to the git root. A session whose cwd you have
- since deleted becomes an unreachable registry row: `sessions close` cannot target it without
- its cwd, so prune it.
+ - Sessions key on **(agent command, absolute cwd, optional name)** - parallel
+ executors need their own directories, which also keeps per-session model and effort
+ settings from racing.
+ - `sessions close` is the teardown verb: marks the record closed, sends ACP
+ `session/close`, takes down the owner and adapter processes (see Teardown). Also
+ required before `codex resume` and `sessions export`.
+ - A session whose cwd you have deleted becomes an unreachable registry row - `close`
+ cannot target it without its cwd; prune it.
## Completion
- **Never poll `status`.** It is a local `kill(pid,0)`-style check on the queue owner and never
- touches the agent. Its states are `running`, `idle`, `dead`, `no-session`, where `dead` means
- the owner is gone or the last exit was abnormal. A turn that finished seconds ago still reads
- `running`, because the owner survives for its idle TTL (default **300s**, `--ttl <seconds>`;
- `--ttl 0` keeps it forever - see Teardown before reaching for that). A loop waiting for
- `idle` will spin past real completion and time out.
-
- What is actually correct:
+ `status` states (the vendor doc has no state table): `running`, `idle`, `dead` (owner
+ gone or last exit abnormal), `no-session`. It is a local `kill(pid,0)`-style check
+ that reads `running` for the whole idle TTL after a turn ends - a loop waiting for
+ `idle` spins past real completion (invariant 3). What is actually correct:
- the foreground stream's terminating `[done] end_turn`, plus the process exit code;
- `sessions show <n>` (`lastActivity`, `historyEntries`) and `sessions history <n>`;
- - best of all, a **result file** the brief required the agent to write - poll for the file.
-
- `--no-wait` returns as soon as the queue owner **acknowledges** the submission - `[queued]
- <id>` in well under a second, even on an idle session. It is not delivery and not completion:
- if a turn is already running, the prompt waits for the turn boundary. Background a normal
- blocking prompt when you want the transcript; use `--no-wait` only to enqueue.
+ - best: a **result file** the brief required the agent to write - poll for the file.
- Supervising a backgrounded run: count tool lines and check mtime
- (`rg -c '^\[tool\]' run.log`, `stat -f '%Sm' run.log`) rather than tailing the whole log, and
- check liveness with `pgrep -fl 'acpx|codex-acp|claude-agent-acp'`.
+ `--no-wait` acknowledges enqueueing (`[queued] <id>`), which is neither delivery nor
+ completion - behind a running turn the prompt waits for the turn boundary.
- `Ctrl+C` (and the `cancel` subcommand) sends ACP `session/cancel` first and force-kills only
- if the agent does not stop in time.
+ Supervise a backgrounded run by counting tool lines and mtime
+ (`rg -c '^\[tool\]' run.log`, `stat -f '%Sm' run.log`); liveness:
+ `pgrep -fl 'acpx|codex-acp|claude-agent-acp'`.
## Teardown
- `sessions close <n>` is the teardown verb, not a bookkeeping nicety: it marks the record
- closed, asks the queue owner to send ACP `session/close`, and takes down the whole resident
- stack (`acpx __queue-owner` -> `npm exec` -> `node <adapter>` -> agent binary). Make it the
- last line of every scripted persistent run. Skipped closes accumulate silently: seven
- forgotten owners from one afternoon of dispatched runs were still holding ~4.9 GB RSS hours
- after their turns ended, every one of them heartbeating a `queueDepth: 0` lock file.
+ `sessions close <n>` takes down the whole resident stack (`acpx __queue-owner` ->
+ `npm exec` -> `node <adapter>` -> agent binary). Make it the last line of every
+ scripted persistent run: forgotten owners hold ~700 MB each for hours, heartbeating
+ `queueDepth: 0` lock files.
- - **Verify, do not assume.** The liveness command from Completion doubles as the leak check:
- `pgrep -fl 'acpx __queue-owner|claude-agent-acp|codex-acp'` should print nothing once your
- runs are closed.
- - **A straggler gets a plain `kill`, never `kill -9` on an owner.** acpx (0.15.1) kills the
- adapter by PID only - there is no process-group kill in the ACP session path - so SIGKILL
- on an owner strands `npm exec -> node -> <agent>` reparented to PID 1, and you are back to
- hunting processes by hand. `sessions close` (or plain SIGTERM, which lets it clean up)
- takes the whole stack with it.
- - **If owners linger with no `--ttl 0` on the command line**, check `~/.acpx/config.json`:
- its `ttl` key sets the default idle TTL, and `"ttl": 0` there disables the self-reap for
- every invocation.
- - **Closed records persist indefinitely.** `sessions prune --older-than 7` on a cadence
- keeps `~/.acpx/sessions` from growing without bound.
+ - **Verify**: `pgrep -fl 'acpx __queue-owner|claude-agent-acp|codex-acp'` should print
+ nothing once your runs are closed.
+ - **A straggler gets a plain `kill`, never `kill -9` on an owner.** acpx (0.15.1)
+ kills the adapter by PID only - there is no process-group kill in the ACP session
+ path - so SIGKILL on an owner strands `npm exec -> node -> <agent>` reparented to
+ PID 1. `sessions close` (or plain SIGTERM) takes the whole stack with it.
+ - **Owners lingering with no `--ttl 0` on the command line**: check
+ `~/.acpx/config.json` - its `ttl` key sets the default idle TTL, and `"ttl": 0`
+ there disables the self-reap for every invocation.
+ - Closed records persist indefinitely; `sessions prune --older-than 7` on a cadence.
## MCP
- `--mcp-config <path>` **replaces** the project/global `mcpServers` for that invocation;
- relative paths inside resolve from `--cwd`.
-
- **The file shape is not the one every other tool uses.** acpx wants a JSON *array* of named
- server objects. Handing it the standard object-keyed map throws an uncaught Node exception
- with a stack trace, not a clean CLI error:
+ `--mcp-config <path>` **replaces** the project/global `mcpServers` for that invocation
+ (relative paths inside resolve from `--cwd`) - and it is absent from the vendor doc
+ entirely.
- ```
- Error: Invalid mcpServers in /path/to/config.json: expected array
- at parseMcpServers (.../acpx/dist/cli.js:1393)
- ```
+ **The file shape is not the standard one.** acpx wants a JSON *array* of named server
+ objects; the standard object-keyed map throws an uncaught Node stack trace:
+ `Error: Invalid mcpServers in <path>: expected array`.
```jsonc
- // WRONG - the Claude Code / standard shape
- { "mcpServers": { "example": { "command": "example", "args": ["mcp"] } } }
-
// RIGHT - an array, each entry carrying its own name
{ "mcpServers": [ { "name": "example", "type": "stdio", "command": "example", "args": ["mcp"] } ] }
```
- ### stdio is the normal case
-
- Most MCP servers are stdio, and acpx passes them straight through - one entry, nothing to run:
-
- ```jsonc
- { "mcpServers": [ { "name": "example", "type": "stdio", "command": "example", "args": ["mcp"] } ] }
- ```
-
- **If a stdio server dies only under agy, it is an MCP era mismatch, not an acpx fault.** Since
- MCP 2026-07-28 (SEP-2575) the `initialize`/`initialized` handshake is retired, and a dual-era
- client is told to probe stdio with `server/discover` first and fall back to `initialize` when
- the answer is not a modern one. A conforming legacy server replies with an error and the
- fallback happens; one that *aborts* on the unexpected request kills the pipe instead, leaving
- the client nothing to fall back from:
-
- ```
- MCP load failed for <name>: Error: failed to start stdio MCP server
- Caused by: expect initialized request, but received: ... method: "server/discover"
- : connection closed: calling "initialize": client is closing: EOF
- ```
-
- agy probes; the codex and claude adapters do not, which is the only reason the same config
- works for them. The fix belongs in the server (answer an error rather than exiting, or move it
- to a current MCP SDK). Reaching it over HTTP also sidesteps it, since there the probe is just
- one failed request rather than a dead process.
-
- That turn still exits **0** and ends `[done] end_turn` - the failure is inline text, not an
- error. Grep for `MCP load failed` before trusting any run that used MCP.
-
- Two more traps:
-
- - **A live queue owner refuses a config change.** The owner carries the config path and a
- SHA-256 fingerprint; switching MCP config on a persistent session requires
- `sessions close` first.
- - **`--mcp-config` replaces the whole set**, so a config written for one adapter is not
- automatically right for another. Verify with a one-shot `exec` that asks the agent to name
- its own tools before dispatching real work.
+ **A stdio server that dies only under agy is an MCP era mismatch, not an acpx fault.**
+ agy probes stdio with `server/discover` (MCP 2026-07-28 retired the `initialize`
+ handshake); a legacy server that *aborts* on the unexpected request kills the pipe
+ instead of answering an error, leaving nothing to fall back from:
+ `MCP load failed for <name>: ... expect initialized request, but received: ...
+ "server/discover"`. The codex and claude adapters do not probe, which is the only
+ reason the same config works for them. Fix the server (answer an error, or move to a
+ current MCP SDK), or reach it over HTTP. The turn still exits **0** and ends
+ `[done] end_turn` - grep for `MCP load failed` before trusting any run that used MCP.
- `${...}` expansion inside an MCP config is evaluated in the **launcher's** environment,
- because the acpx child inherits it - so a variable like a session id resolves to *your* id,
- not the child's. Never let an identity flow in through the environment.
+ - **A live queue owner refuses a config change** (it carries the config path plus a
+ SHA-256 fingerprint): `sessions close` first.
+ - **`--mcp-config` replaces the whole set** - verify a config written for one adapter
+ on another with a one-shot `exec` that asks the agent to name its own tools.
+ - `${...}` in an MCP config expands in the **launcher's** environment (the child
+ inherits it), so a variable like a session id resolves to *your* id. Never let an
+ identity flow in through the environment.
## Limits
- - **Incoming ACP messages are capped at 64 MiB** since 0.15.1 (previously unlimited). Raise
- with `ACPX_MAX_ACP_MESSAGE_BYTES`, or `0` to disable. A previously-working large-payload
- script can start failing here.
- - Terminal output retention is 64 KiB per call; `ACPX_TERMINAL_MAX_OUTPUT_BYTES` adjusts it.
- - `--timeout <seconds>` is the wall clock for the whole prompt. Long unattended work wants
- `--timeout 5400`; a wrapping shell `timeout` is a reasonable belt-and-braces. Do not add
- `--ttl 0`: the TTL only governs how long an idle owner lingers between prompts, the 300s
- default already covers any gap within one run, and `--ttl 0` removes the only automatic
- reaper (invariant 6). Reserve it for a human-driven warm session you will close by hand.
- - Output: `--format quiet` for many backgrounded executors, `text` for one you are watching,
- `json` (with `--json-strict`) when a script parses it. `--suppress-reads` keeps read-file
- contents out of the log and is worth setting on every long run.
- - Pass long briefs with `-f <path>` or a path in the prompt, not inlined text: it keeps the
- content out of the shell command, where a driving harness's classifier may block on it.
+ - **Incoming ACP messages cap at 64 MiB** since 0.15.1 (previously unlimited);
+ `ACPX_MAX_ACP_MESSAGE_BYTES` raises it, `0` disables.
+ - Terminal output retention is 64 KiB per call (`ACPX_TERMINAL_MAX_OUTPUT_BYTES`).
+ - Long unattended work: `--timeout 5400`, plus a wrapping shell `timeout` as
+ belt-and-braces. Do NOT add `--ttl 0` - the 300s default covers any gap within one
+ run, and `--ttl 0` removes the only automatic reaper (invariant 6). Reserve it for a
+ human-driven warm session you will close by hand.
+ - `--format quiet` for fleets; `--suppress-reads` on every long run.
+ - Pass long briefs as `-f <path>` or a path in the prompt, never inlined - inlined
+ content in a shell command is what a driving harness's classifier blocks on.
## What acpx cannot do
Structural, not bugs. Do not design around them.
- - **An ACP session does not wake.** An externally injected message lands in the transcript but
- starts no turn, because under ACP the turn loop belongs to the ACP client. The only inbound
- channel to an acpx session is acpx itself (`-s <name>`, optionally `--no-wait`).
- - **`codex queue --thread` does not reach an acpx-driven codex session**, though it does wake a
- plain interactive codex. The ACP wrapper is not in the app-server's context.
- - **acpx-spawned Claude sessions bind no IPC socket** (`entrypoint: sdk-cli`, no key file), so
- `ListAgents` / `SendMessage` cannot see or reach them.
- - **acpx cannot attach to a session it did not start.** It owns the process it drives; a
- human-started headful session is out of reach.
- - **Permission flags are not a sandbox.** See invariant 4: the claude adapter already passed
- `--allow-dangerously-skip-permissions` to its binary, so no permission request ever reaches
- acpx's policy layer. Verify it yourself with `ps -Ao args | grep claude-agent-sdk` during a
- turn. `--no-terminal` genuinely removes the terminal capability (an agent that calls it gets
- a hard error), but for filesystem safety the only real control is which `--cwd` you hand it.
+ - **An ACP session does not wake.** An externally injected message lands in the
+ transcript but starts no turn; the only inbound channel is acpx itself
+ (`-s <name>`, optionally `--no-wait`).
+ - **`codex queue --thread` does not reach an acpx-driven codex session** (it does wake
+ a plain interactive codex).
+ - **acpx-spawned Claude sessions bind no IPC socket**, so `ListAgents` /
+ `SendMessage` cannot see or reach them.
+ - **acpx cannot attach to a session it did not start.**
## Failures
- Exit codes, which the shipped doc does not list:
+ Exit codes (absent from the vendor doc):
| code | meaning |
|---|---|
| 0 | success (also `cancel` with nothing to cancel) |
| 1 | agent / protocol / runtime error - the catch-all |
| 2 | usage error: bad or conflicting flags, malformed `--agent` |
| 3 | `--timeout` exceeded |
| 4 | no session found by the directory walk |
- | 5 | every permission request denied or cancelled, none approved |
+ | 5 | every permission request denied or cancelled |
| 130 | interrupted (cooperative cancel first) |
- **`โ No acpx session found (searched up to <cwd>).`** (exit 4) - invariant 2. Emitted by
- `set`, by `-s` prompts, by anything that resolves a session. Fix:
- `sessions ensure --name <n>` first. Confirm `--cwd` is the directory you think it is.
-
- **`error: unknown option '--cwd'`** (exit 2) - a global flag placed after the agent
- subcommand. Move it before.
-
- **`Do not combine positional agent with --agent override`** (exit 2) - drop the positional
- `claude`/`codex` when `--agent` supplies the adapter.
-
- **`Invalid mcpServers in <path>: expected array`** (uncaught, stack trace) - the MCP config is
- the standard object map; convert it to an array. See MCP.
-
- **`Invalid value for config option model: <id> (ACP -32603, adapter reported "Internal error")`**
- - `set model` with an id outside the adapter's advertised list. Use `--model` at session
- creation, or pick an advertised id from `--format json ... status`.
-
- **`RUNTIME: Internal error: There's an issue with the selected model (<id>)`** - the harness
- itself rejects the id. This is a real typo or an entitlement problem, not the previous case.
-
- **`Internal error`** from `claude set reasoning_effort` - there is no effort knob on the
- claude adapter. Expected; ignore.
-
- **`[error] RUNTIME: Authentication required`** - the agy ACP server has never been
- authenticated. Write `auth.type` into `~/.gemini/antigravity-acp/settings.json`, then complete
- its own sign-in; the CLI's token does not count. See agy.
-
- **`502 Bad Gateway: Failed to connect to backend API`** (agy ACP, turn still ends
- `[done] end_turn` exit 0) - the server's local proxy could not reach
- `cloudcode-pa.googleapis.com`. On NixOS this is a missing `SSL_CERT_FILE`, not a network or
- model problem; confirm with `--alsologtostderr` and look for `CERTIFICATE_VERIFY_FAILED`.
-
- **`authenticate` never returns** (agy ACP, no error, no URL) - no `auth.type` in
- `~/.gemini/antigravity-acp/settings.json`. `ACPX_AUTH_OAUTH_PERSONAL=1` no longer selects
- anything in this build.
-
- **`RUNTIME: This client is no longer supported for Gemini Code Assist for individuals`** - you
- used the built-in `gemini` agent. It is the public Gemini CLI, not Antigravity; use `--agent`
- with the `.par` server.
-
- **`already has an active writer (code -32600)`** - `codex resume` while acpx still owns the
- thread. `sessions close <n>` first.
-
- **`Missing --skill action.`** (exit 1) - `--skill` needs `show`, `list`, `install`, `export`
- or `help`.
-
- **`error: unknown option '--one-shot'`** (exit 2, usage on **stderr**) - not a verb; `exec` is
- the one-shot form.
+ Error catalog - string -> fix:
- **Timeouts** (exit 3) mean the wall clock ran out, not that the agent is stuck. Check
- `sessions show` for `lastActivity` before assuming failure, and remember an adapter-side
- timeout surfaces as exit 1 instead.
+ - `No acpx session found (searched up to <cwd>)` (exit 4) - invariant 2:
+ `sessions ensure --name <n>` first; confirm `--cwd`.
+ - `error: unknown option '--cwd'` (exit 2) - global flag after the subcommand
+ (invariant 1).
+ - `Do not combine positional agent with --agent override` (exit 2) - drop the
+ positional `claude`/`codex`.
+ - `Invalid mcpServers in <path>: expected array` (uncaught stack trace) - see MCP.
+ - `Invalid value for config option model: <id> (ACP -32603, adapter reported
+ "Internal error")` - `set model` outside the advertised list; use `--model` at
+ session creation (see claude).
+ - `RUNTIME: Internal error: There's an issue with the selected model (<id>)` - the
+ harness itself rejects the id: a real typo or an entitlement problem.
+ - `Internal error` from `claude set reasoning_effort` - no effort knob; expected.
+ - `[error] RUNTIME: Authentication required` - the agy ACP server was never
+ authenticated; see agy.
+ - `502 Bad Gateway: Failed to connect to backend API` (agy, exit 0) - missing CA
+ bundle on NixOS; see Linux / NixOS.
+ - `authenticate` never returns (agy, no error, no URL) - no `auth.type` in
+ `~/.gemini/antigravity-acp/settings.json`; see agy.
+ - `RUNTIME: This client is no longer supported for Gemini Code Assist` - you used the
+ built-in `gemini` agent; use `--agent` with the `.par`.
+ - `already has an active writer (code -32600)` - `codex resume` while acpx still owns
+ the thread; `sessions close <n>` first.
+ - `Missing --skill action.` (exit 1) - `--skill` needs
+ `show|list|install|export|help`.
+ - `error: unknown option '--one-shot'` (exit 2) - not a verb; `exec` is the one-shot
+ form.
+ - Exit 3 = the wall clock ran out, not a stuck agent - check `sessions show` for
+ `lastActivity`; an adapter-side timeout surfaces as exit 1 instead.
- Silent failures worth an explicit check: a content-filter kill and an MCP load failure both
- end `[done] end_turn` with exit 0; a `--no-wait` prompt that "sent" may still be queued behind
- a running turn; `status` reporting `running` long after completion; and a session record whose
- cwd no longer exists lingering until pruned.
+ Silent failures worth an explicit check: content-filter kills and MCP load failures
+ both end `[done] end_turn` with exit 0; a `--no-wait` prompt that "sent" may still be
+ queued behind a running turn; `status` reads `running` long after completion; a
+ session whose cwd no longer exists lingers until pruned.