v0.2: multi-turn /sessions endpoints backed by ACPX
- Dockerfile: install acpx@latest alongside @anthropic-ai/claude-code
- compose.yml: bind /mnt/user/appdata/clawdforge/acpx-sessions:/root/.acpx/sessions
- DB: additive sessions + session_events tables in store.py SCHEMA
- clawdforge/acpx_runner.py: AcpxManager + AcpxSession, bounded async pool,
per-invocation subprocess model (acpx CLI itself owns the queue-owner
lifecycle, so each turn = one fresh `acpx prompt -s <uuid>` call)
- server.py: POST/GET/DELETE /sessions + POST /sessions/{id}/turn + GET /sessions
- Per-app isolation: 404 (not 403) on cross-token session access — no
existence leak across tokens
- Lifespan-managed TTL sweeper: every 60s soft-closes idle sessions past
CLAWDFORGE_SESSION_TTL_SECS (1h default), hard-deletes ledger rows past
CLAWDFORGE_SESSION_HARD_TTL_SECS (24h default)
- session_events audit table parallel to existing runs table
(events: create, turn, close, sweep_close, hard_delete)
- /healthz now reports acpx_present + acpx_version + open_sessions count
- tests/test_sessions.py: 16 tests covering create/turn/close/list/isolation/
sweep/pool-full/regression. /run regression test asserts byte-identical
v0.1 response shape.
ACPX research notes (v0.6.1, openclaw/acpx):
- npm package is `acpx`, not `@openclaw/acpx`
- Sessions are scoped by (agentCommand, cwd, name?). We mint our own UUID
as `--name` and give every session a unique cwd subdir, so the scope key
is collision-free across apps.
- session_id source: ours. We pass --name <uuid>, ACPX records it under
~/.acpx/sessions/<encoded-id>.json. We never need to parse ACPX's
acpxRecordId — our UUID is canonical.
- Subprocess lifetime: per-invocation, NOT per-session. The acpx CLI itself
spawns/maintains a per-session "queue owner" process via local IPC; each
`acpx prompt` call we make either elects itself owner or enqueues. The
AcpxSession class is therefore a thin (uuid, cwd, asyncio.Lock) handle,
not a long-lived stdio pipe. The spec's "owns one stdio pipe pair" model
was rewritten to match reality — flagged here.
- Close semantics: soft-close via `acpx sessions close <name>`. The
on-disk record stays (ACPX's `sessions prune` is the hard-delete path,
not invoked from clawdforge). DELETE /sessions/<id> is documented as
idempotent (200 with already_closed=true on second call) so SDKs can
call close() in finally/Drop blocks safely.
- File uploads: ACPX has no file-attach ACP method exposed via the CLI.
We prepend a [Attached files] header listing absolute paths; the agent
uses its Read tool to open them. Same behavior as /run --files in v0.1.
- Permissions: --approve-all on the turn invocation since the container is
unattended and callers are bearer-token-trusted. Future v0.3 may expose
a per-session permission policy.
/run endpoint unchanged — backwards compat verified by
test_run_endpoint_unchanged + test_run_endpoint_unchanged_error_shape.
Spec: memory/spec-clawdforge-v0.2.md
ACPX CLI ref: https://github.com/openclaw/acpx/blob/main/docs/CLI.md
This commit is contained in:
parent
19fe299b3d
commit
940861f70a
11 changed files with 1829 additions and 20 deletions
111
README.md
111
README.md
|
|
@ -17,6 +17,13 @@ tokens + IP allowlist.
|
|||
GET /healthz liveness + claude --version smoke
|
||||
POST /run run a prompt, return parsed result
|
||||
POST /files upload a file, get a file_token to pass to /run
|
||||
|
||||
POST /sessions create a multi-turn session (v0.2)
|
||||
POST /sessions/<id>/turn send a turn to a session (v0.2)
|
||||
GET /sessions/<id> read session state (v0.2)
|
||||
DELETE /sessions/<id> soft-close a session (v0.2)
|
||||
GET /sessions list this token's sessions (v0.2)
|
||||
|
||||
POST /admin/tokens mint a per-app token (admin)
|
||||
GET /admin/tokens list app tokens (admin)
|
||||
DELETE /admin/tokens/<name> revoke a token (admin)
|
||||
|
|
@ -115,10 +122,112 @@ r.raise_for_status()
|
|||
print(r.json()["result"]) # {'hello': 'world'}
|
||||
```
|
||||
|
||||
## Multi-turn / Sessions (v0.2)
|
||||
|
||||
`/run` is one-shot: stateless, fast, returns a single result. When you need
|
||||
multi-turn context (build something step-by-step, debug across iterations,
|
||||
long-running structured tool-call work), use `/sessions/*`.
|
||||
|
||||
The session surface is backed by [ACPX](https://github.com/openclaw/acpx) —
|
||||
the OpenClaw team's headless Agent Client Protocol CLI. Clawdforge wraps it
|
||||
so apps don't need to manage ACPX subprocesses or session metadata directly.
|
||||
Sessions persist on disk under `/root/.acpx/sessions/` (mounted from the
|
||||
host) so they survive container rebuilds.
|
||||
|
||||
### Quickstart — three turns in one session
|
||||
|
||||
```bash
|
||||
TOKEN=$CLAWDFORGE_TOKEN
|
||||
CF=http://192.168.0.5:8800
|
||||
|
||||
# 1. Create a session
|
||||
SID=$(curl -sS -X POST $CF/sessions \
|
||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"agent":"claude"}' | jq -r .session_id)
|
||||
|
||||
# 2. Send a turn
|
||||
curl -sS -X POST $CF/sessions/$SID/turn \
|
||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"prompt":"Help me draft a SQL migration. First describe what you need to know."}'
|
||||
|
||||
# 3. Send a follow-up — the session keeps context
|
||||
curl -sS -X POST $CF/sessions/$SID/turn \
|
||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"prompt":"Postgres 16, table users, add column tier text default \"free\""}'
|
||||
|
||||
# 4. Close
|
||||
curl -sS -X DELETE $CF/sessions/$SID -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### Turn response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"session_id": "abc123...",
|
||||
"turn_index": 2,
|
||||
"events": [
|
||||
{"jsonrpc":"2.0","method":"session/update","params":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"..."}}},
|
||||
{"jsonrpc":"2.0","id":"req-1","result":{"stopReason":"end_turn"}}
|
||||
],
|
||||
"stop_reason": "end_turn",
|
||||
"duration_ms": 12345
|
||||
}
|
||||
```
|
||||
|
||||
`events` is the raw ACP NDJSON stream from acpx, parsed into objects. Each
|
||||
entry is a JSON-RPC message — `session/update` for streamed agent output,
|
||||
tool calls, plan updates, etc., and a final `result` envelope with the
|
||||
`stopReason`. No streaming/SSE in v0.2; the full event list is returned
|
||||
when the turn ends.
|
||||
|
||||
### Per-app isolation
|
||||
|
||||
Every session is owned by the token that created it. Cross-token access
|
||||
returns `404` (not `403`) so token A can't even probe whether token B's
|
||||
session exists.
|
||||
|
||||
### TTL + cleanup
|
||||
|
||||
A background sweeper runs every `CLAWDFORGE_SWEEP_INTERVAL_SECS` (default 60s):
|
||||
|
||||
- Sessions idle longer than `CLAWDFORGE_SESSION_TTL_SECS` (default 1h) are
|
||||
soft-closed via `acpx sessions close`.
|
||||
- Sessions whose `closed_at` is older than `CLAWDFORGE_SESSION_HARD_TTL_SECS`
|
||||
(default 24h) are hard-deleted from clawdforge's ledger.
|
||||
- Closed sessions stay queryable via `GET /sessions/<id>` until the hard-TTL
|
||||
fires.
|
||||
|
||||
### Container / deploy
|
||||
|
||||
The container needs both `claude` and `acpx` on PATH plus a host-mounted
|
||||
volume for ACPX's session store:
|
||||
|
||||
```yaml
|
||||
# compose.yml (already configured)
|
||||
volumes:
|
||||
- /mnt/user/appdata/clawdforge/acpx-sessions:/root/.acpx/sessions
|
||||
```
|
||||
|
||||
```dockerfile
|
||||
ENV ACPX_BIN=acpx
|
||||
RUN npm install -g acpx@latest
|
||||
```
|
||||
|
||||
ACPX shares Claude Code auth from the same `/root/.claude/` volume the v0.1
|
||||
runtime already used, so a single `claude /login` ceremony covers both
|
||||
`/run` and `/sessions/*`.
|
||||
|
||||
## Notes
|
||||
|
||||
- The CLI is `@anthropic-ai/claude-code` (not the Python `anthropic` SDK).
|
||||
- Default model is `sonnet`; per-request override via `model` field.
|
||||
- ACPX is the upstream session driver — see https://github.com/openclaw/acpx
|
||||
and `docs/CLI.md` in that repo for protocol semantics. Clawdforge owns
|
||||
the per-app ledger and TTL policy; ACPX owns session content.
|
||||
- Default model is `sonnet`; per-request override via `model` field on `/run`.
|
||||
For sessions, model is fixed at create time (configurable later).
|
||||
- Per-run working directory is staged under `RUNS_DIR` and torn down on exit, so
|
||||
`claude` can't pollute the container's working tree.
|
||||
- Per-session working directory is staged under `ACPX_SESSIONS_CWD` (default
|
||||
`/data/acpx-cwds/<session_id>`) and torn down on close.
|
||||
- File uploads are scoped to the uploading app — token A can't reference token B's files.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue