Retry-safe writes

Send a request ID with capture, remember, or a document save so a retry after a lost response finishes the same write instead of duplicating or overwriting it.

A timeout, dropped connection, crash, or server restart can leave a caller unsure whether a write landed. Attach an optional request ID to a capture, a remember, or a document save and GNO records it in a private local ledger. Retrying with the same ID returns the original outcome, or finishes an interrupted write, instead of creating a second note, superseding a fact twice, or overwriting a newer edit.

Callers that send no request ID keep their response shapes, with one documented exception: capture and document saves now run under the shared write lease, so a busy lease returns the typed busy error (409 LOCKED for REST saves) and two saves from the same revision cannot both win. A CLI or SDK capture whose lexical sync fails still comes back as a receipt with sync.status: "failed". See Without a request ID.

Where request IDs are accepted

OperationCLIMCPRESTSDK
Capturegno capture ... --request-id <id>gno_capture argument requestIdPOST /api/capture body requestIdclient.capture({ ..., requestId })
Remember (add or supersede)gno remember ... --add|--supersede ... --request-id <id>gno_remember argument requestIdPOST /api/memory/remember body requestIdclient.remember({ ..., requestId })
Document save (content and tags)not availablenot availablePUT /api/docs/:id body requestIdnot available

Check before you retry

  1. Generate a fresh ID (a UUID) for each write intent and keep it before you send the request.
  2. After a timeout or a lost response, look the ID up before doing anything else.
  3. committed: use the recorded result and do not resend. pending: resend the exact same call with the same ID; GNO finishes it. not_found: nothing was accepted under the ID; resend with the same ID. expired: the ID already ran and will not run again; check current state before choosing a new ID.
  4. Never reuse an ID for a changed payload. A new intent gets a new ID.
id=$(uuidgen)
gno capture --file ./call-notes.md --collection notes --request-id "$id" --json
# The terminal closed before the result arrived.
gno request-status "$id"
# pending or not_found: run the identical command again with the same ID.
gno capture --file ./call-notes.md --collection notes --request-id "$id" --json
SurfaceLookup
CLIgno request-status <request-id> [--json]
MCPgno_request_status { requestId }, read-only, registered with --enable-write on the full tool profile (not in core)
RESTGET /api/requests/:requestId
SDKclient.requestStatus(requestId)
{
  "requestId": "3f2c9a0e-5b1d-4c7a-9e21-6d0f8a4b7c11",
  "status": "committed",
  "operation": "capture",
  "createdAt": "2026-09-24T08:00:00.000Z",
  "updatedAt": "2026-09-24T08:00:00.120Z",
  "result": {
    "uri": "gno://notes/inbox/call-notes.md",
    "docid": "#abc123",
    "contentHash": "..."
  }
}

What a write returns with a request ID

A successful write that carried an ID gains one field. It is absent when no ID was sent.

"request": {
  "requestId": "3f2c9a0e-5b1d-4c7a-9e21-6d0f8a4b7c11",
  "status": "committed",
  "replayed": false,
  "committedAt": "2026-09-24T08:00:00.120Z"
}

Same ID, same intent

When a write counts as committed

OperationCommitted when
CaptureThe note file is written and lexically synced, so it is retrievable.
Remember addThe fact file is written and lexically synced.
Remember supersedeThe successor is written and synced and its supersedes edge is projected.
Document saveThe source file is written and its new sourceHash is recorded, along with any tag changes.

A committed document save means the canonical file on disk is updated. Lexical search catches up in a deferred sync job and embeddings follow through the scheduler. Neither is part of the commit, and a failure in either never undoes the save: run gno update or gno embed to recover. A retried exact-duplicate remember --add is recorded as its existing outcome and replays as such.

Rejected or interrupted

The guarantee is one durable local side effect per admitted request for these writes. It makes no claim about external systems, and the deferred index refresh and embeddings after a document save are separate work.

Error codes

CodeMeaningHTTPCLI exit
REQUEST_ID_INVALIDMalformed or oversized ID, or an ID on a non-write call4001
REQUEST_ID_CONFLICTThe ID was already used for a different intent4091
REQUEST_EXPIREDThe ID was used before and its full receipt has expired; it will not run again4101
REQUEST_PENDINGAccepted and still in progress elsewhere; retry the same ID later4094
REQUEST_RECOVERY_CONFLICTThe recorded write’s target changed or disappeared after it was written; nothing was overwritten or recreated4092
REQUEST_CAPACITY_EXHAUSTEDThe ledger is full; rejected before any write5072
REQUEST_LEDGER_UNAVAILABLEThe ledger cannot be opened; rejected before any write5032

REQUEST_RECOVERY_CONFLICT, CONFLICT, and MEMORY_PREDECESSOR_HASH_MISMATCH all mean the same thing for a caller: read the current document or recall the current fact, then decide again. Never force an overwrite.

Who can see a request

Local ledger, retention, and privacy

Each index has one private SQLite ledger next to its index database, at <data dir>/write-receipts/<index db filename>:

On POSIX systems the directory is created 0700 and the file 0600. The ledger is local state: nothing in it leaves your machine.

Web UI

The capture dialog and document saves (editor autosave, Cmd/Ctrl+S, and tag saves) generate one request ID per submitted intent and reuse it while the same payload is retried, for example with Try again after a network error. Edited content or different tags get a new ID, and after a success the next submit is a new intent.

A save whose response was lost and is then retried gets the committed result instead of a false “Document changed on disk” conflict. Pending and conflict errors show the server message. Stale edits still get CONFLICT and are never force-overwritten. The unconfirmed request ID is kept for the browser tab, so retrying the same capture or save after a page refresh still replays instead of writing twice; after a refresh the editor reloads the current document from disk.

Without a request ID

Writes without an ID keep their response shapes and outcomes, with a weaker guarantee when a response is lost and the caller retries:

One change applies to every client: capture and document saves run under the shared write lease, and the SDK capture takes it too.