Skip to main content
A callback gate records a question that code cannot answer by itself. A router claims the pending request, asks a person or service, and submits one structured answer. The callback client records each step so another process can rebuild the same state.

Run the example

From an Obversa checkout, run:
The example proves that only one router can hold a claim, a released request can be claimed again, an accepted answer survives replay, and changing the question bytes creates a new request ID.

Source

The clean-consumer check compiles and runs this exact source from the packed package with TypeScript 6 and TypeScript 7.

Request identity

The request digest covers the gate ID and version, the question text, the response schema, and the input bytes. The request ID contains that digest. The same question therefore keeps the same ID across process restarts. Changed question bytes produce a new ID. Presentation hints do not change either value.

Claim, submit, and release

  1. Post: post records the request as pending.
  2. Claim: claim assigns it to exactly one router and returns a claim token. The token is recorded in the replayable event history.
  3. Submit: submit accepts an answer only from that router with that token and the posted digest.
  4. Release: release returns a claimed request to the pending list. The directRouter helper also releases the claim if its responder throws or submits an invalid response.
Claim refusals report missing, claimed, answered, or superseded. Submission refusals report missing, not-claimed, not-owner, stale, or invalid. Release refusals report missing or not-owner.

Validation and storage limit

The built-in validator checks required top-level fields and the standard JSON types declared for top-level fields. It accepts a single type or a type union. It is not a full JSON Schema validator. A host that needs nested constraints must validate them before it submits the answer. createCallbackClient keeps its event history in memory. Replay proves the history contains all callback state, but the host must store that history when requests must survive a process exit.

Store callbacks with a run

createStoredCallbackClient(storage, runId) writes callback history into the run’s event stream. Opening a new client on the same run rebuilds pending, claimed, released, submitted, and superseded state from those events. The stored client uses the event stream revision when it changes state. Two routers that race for one request therefore produce one winning claim. A storage failure does not leave an in-memory answer that was never recorded. Stored replay accepts only the exact fields for each callback event. A callback-submitted event contains kind, requestId, requestDigest, routerId, and response. The request digest must be 64 lowercase hexadecimal characters and match the posted request. A submission without requestDigest, or with an extra field, does not replay. The runtime does not convert that stored history. The stored client records the answer. The host must turn that answer into a graph node result. The graph executor does not do this.

Bind approval to exact bytes

createApprovalCallbackGate(definition, subject) adds the subject digest to the request input before it creates the request ID. The subject names the input artifact hashes, proof scope and artifact, proposed output bytes, effective permissions, and a workspace anchor or null when no workspace is bound. Changed subject bytes therefore create a different request. Post that request with the same subject:
The stored client refuses a missing or different subject with ApprovalSubjectError, whose code is SUBJECT_MISMATCH. A subject-backed submission also needs caller-supplied actor data and an action decision of allow, wait, or deny. The client validates the response, then writes the callback submission and approval record in one event batch. A repeated plain submission after the request is answered returns the typed not-claimed result. For a subject-backed request, a missing actor or invalid action decision returns the typed invalid result. After a valid subject-backed answer, the same submission from the claim owner returns success and adds no event. A different valid decision or actor from that owner throws StorageError with REVISION_CONFLICT. A wrong claim owner returns the typed not-owner result. A changed request digest from the claim owner returns the typed stale result. resolveApproval(storage, runId, { request, ...subject }) returns the stored decision only while the request, run plan, graph, proof, inputs, proposed output, permissions, and workspace anchor still match. Otherwise it returns a wait decision. The actor data records what the caller supplied; this API does not authenticate that actor. An approval record does not apply the proposed change.