fix(ce-proof): correct op shapes and add retry/batch discipline (#658)
This commit is contained in:
@@ -79,9 +79,21 @@ All operations go to `POST https://www.proofeditor.ai/api/agent/{slug}/ops`
|
||||
|
||||
**Wire-format reminder.** `/api/agent/{slug}/ops` uses a top-level `type` field; `/api/agent/{slug}/edit/v2` uses an `operations` array where each entry has `op`. Do not mix — sending `op` to `/ops` returns 422.
|
||||
|
||||
**Every mutation requires a `baseToken`.** Read it from `/state.mutationBase.token` (or `/snapshot.mutationBase.token`) immediately before each write, and include it in the request body. On `BASE_TOKEN_REQUIRED` or `STALE_BASE`, re-read and retry once. See the baseToken recipe in `references/hitl-review.md`.
|
||||
**Every mutation requires a `baseToken`.** Reuse the `mutationBase.token` from the most recent `/state` or `/snapshot` read — tokens don't go stale in seconds, and `STALE_BASE` is a recoverable error. On `BASE_TOKEN_REQUIRED` or `STALE_BASE`, re-read and retry once. Only do a pre-mutation read if no prior read has happened in this session. See the baseToken recipe in `references/hitl-review.md`.
|
||||
|
||||
**`Idempotency-Key` header** is recommended on every mutation for safe automation retries; required when `/state.contract.idempotencyRequired` is true.
|
||||
`/edit/v2` block refs are a separate concern: they can drift across revisions, so re-fetch `/snapshot` for fresh refs before a block edit if any writes have landed since your last snapshot.
|
||||
|
||||
**Retry discipline after mutation errors — verify before retrying.** An error response is not proof that nothing was written.
|
||||
|
||||
- `STALE_BASE`, `BASE_TOKEN_REQUIRED`, `MISSING_BASE`, `INVALID_BASE_TOKEN` — pre-commit, token-related. Re-read `/state` and retry once with the same payload and a fresh `baseToken`. A generic mutate helper can auto-retry these.
|
||||
- `ANCHOR_NOT_FOUND`, `ANCHOR_AMBIGUOUS` — pre-commit, but the `quote` no longer uniquely matches content. Re-reading does not help by itself; the caller must tighten or regenerate the anchor before retrying. Do not auto-retry blindly.
|
||||
- `INVALID_OPERATIONS`, `INVALID_REQUEST`, `INVALID_REF`, `INVALID_BLOCK_MARKDOWN`, `INVALID_RANGE`, `INVALID_MARKDOWN`, 422 — pre-commit, but the payload is wrong. Do not retry blindly; fix the payload first.
|
||||
- `COLLAB_SYNC_FAILED`, `REWRITE_BARRIER_FAILED`, `PROJECTION_STALE`, `INTERNAL_ERROR`, 5xx, network timeout, and any **202 with `collab.status: "pending"`** — the canonical doc may have been written even though the call looks like a failure. Before any retry, re-read `/state` and check whether the intended mark/edit is already present; only retry if it isn't.
|
||||
- `Idempotency-Key` (see below) protects against double-apply *on the same request* (e.g., TCP-level retry). It does not help if you build a new request body and send a second call — that is a new logical write with a new key.
|
||||
|
||||
Duplicate-mark incidents usually come from retrying a `comment.add` or `suggestion.add` after a timeout without verifying. When in doubt: re-read, diff, then decide.
|
||||
|
||||
**`Idempotency-Key` header** is recommended on every mutation for safe automation retries; required when `/state.contract.idempotencyRequired` is true. Use the same key when retrying the exact same logical write (same payload) so the server can collapse the retry. A new key means a new write — even if the payload is identical.
|
||||
|
||||
**Comment on text:**
|
||||
```json
|
||||
@@ -136,12 +148,23 @@ curl -X POST "https://www.proofeditor.ai/api/agent/{slug}/edit/v2" \
|
||||
"baseToken": "mt1:<token>",
|
||||
"operations": [
|
||||
{"op": "replace_block", "ref": "b3", "block": {"markdown": "Updated paragraph."}},
|
||||
{"op": "insert_after", "ref": "b3", "block": {"markdown": "## New section"}}
|
||||
{"op": "insert_after", "ref": "b3", "blocks": [{"markdown": "## New section"}]}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Supported `op` kinds inside `operations`: `replace_block`, `insert_before`, `insert_after`, `delete_block`, `replace_range` (uses `fromRef` + `toRef`), `find_replace_in_block` (takes `occurrence: "first" | "all"`). Read `/snapshot` to get stable block `ref` IDs and the `mutationBase.token`.
|
||||
Per-op body shape (singular `block` vs plural `blocks` is load-bearing — sending the wrong one returns 422):
|
||||
|
||||
| op | body fields |
|
||||
|---|---|
|
||||
| `replace_block` | `ref`, `block: {markdown}` |
|
||||
| `insert_after` | `ref`, `blocks: [{markdown}, ...]` |
|
||||
| `insert_before` | `ref`, `blocks: [{markdown}, ...]` |
|
||||
| `delete_block` | `ref` |
|
||||
| `replace_range` | `fromRef`, `toRef`, `blocks: [{markdown}, ...]` |
|
||||
| `find_replace_in_block` | `ref`, `find`, `replace`, `occurrence: "first" \| "all"` |
|
||||
|
||||
Read `/snapshot` to get stable block `ref` IDs. `operations` commits atomically — either every op lands or none do — so one `/edit/v2` call can batch dozens of block edits safely and efficiently (see the bulk-sweep guidance in `references/hitl-review.md` Phase 2.4).
|
||||
|
||||
**Editing while a client is connected is fine.** `/edit/v2`, `suggestion.add` (including `status: "accepted"`), and all comment ops work during active collab. Only `rewrite.apply` is blocked by `LIVE_CLIENTS_PRESENT` — it would clobber in-flight Yjs edits.
|
||||
|
||||
@@ -193,13 +216,11 @@ When given a Proof URL like `https://www.proofeditor.ai/d/abc123?token=xxx`:
|
||||
4. The author sees changes in real-time
|
||||
|
||||
```bash
|
||||
# Read
|
||||
curl -s "https://www.proofeditor.ai/api/agent/abc123/state" \
|
||||
-H "x-share-token: xxx"
|
||||
|
||||
# Get baseToken for the next mutation
|
||||
BASE=$(curl -s "https://www.proofeditor.ai/api/agent/abc123/state" \
|
||||
-H "x-share-token: xxx" | jq -r '.mutationBase.token')
|
||||
# Read once — the same response yields both the doc content and the baseToken for every mutation below.
|
||||
STATE=$(curl -s "https://www.proofeditor.ai/api/agent/abc123/state" \
|
||||
-H "x-share-token: xxx")
|
||||
BASE=$(printf '%s' "$STATE" | jq -r '.mutationBase.token')
|
||||
# Inspect doc fields as needed: printf '%s' "$STATE" | jq '.markdown, .revision'
|
||||
|
||||
# Comment
|
||||
curl -X POST "https://www.proofeditor.ai/api/agent/abc123/ops" \
|
||||
@@ -291,4 +312,4 @@ rm "$STATE_TMP"
|
||||
- During active collab use `edit/v2` (direct block changes) or `suggestion.add` (tracked changes); reserve `rewrite.apply` for no-client scenarios since it's blocked by `LIVE_CLIENTS_PRESENT` when anyone is connected
|
||||
- Don't span table cells in a single replace
|
||||
- Always include `by: "ai:compound-engineering"` on every op and `X-Agent-Id: ai:compound-engineering` in headers for consistent attribution
|
||||
- Read a fresh `baseToken` before every mutation; on `STALE_BASE`, re-read and retry once
|
||||
- Reuse `baseToken` from your most recent `/state` or `/snapshot` read; on `STALE_BASE`, re-read and retry once
|
||||
|
||||
Reference in New Issue
Block a user