Run events

The persisted transition log, and how to resume it after a disconnect.

The transition ledger

A compile publishes its transitions to an append-only ledger. The event stream replays that ledger from Last-Event-ID and then follows it, so a client that reconnects sees everything it missed rather than the current state alone.

GET/compile-jobs/{jobId}/eventsScope collections:read

Replays the persisted transition log after Last-Event-ID, then follows it. The server closes the stream at its own wall clock, so reconnecting is the normal case rather than an error path; every frame carries the durable sequence to resume from. Nothing is lost when a connection drops, because the events are written before they are sent and nothing was ever only in the connection.

curl -sS -X GET https://tavonel.com/api/compile-jobs/{jobId}/events \
  -H "Authorization: Bearer $TAVONEL_API_KEY"
StatusResponse
200text/event-stream of persisted compile transitions. Each frame's id is the durable sequence; echo it back as Last-Event-ID to resume.
400COMPILE_JOB_SCOPE_INVALID — The job exists and belongs to another workspace.
401AUTH_REQUIRED — No credential was presented, or the bearer token is not a key TAVONEL issued. This is what an unauthenticated request to any scoped route returns.
404COMPILE_JOB_NOT_FOUND — No such job in this workspace. Job ids are workspace-scoped, so this is also the answer for another tenant's id.
GET/compile-jobs/{jobId}Scope collections:read

The durable current state. A poller against this sees exactly what a stream subscriber sees, because both read the same row. A job id from another workspace answers not found, which is also all it should reveal.

curl -sS -X GET https://tavonel.com/api/compile-jobs/{jobId} \
  -H "Authorization: Bearer $TAVONEL_API_KEY"
StatusResponse
200{ code: OK, job }, including any blocked documents and the resolution recorded against them.
400COMPILE_JOB_SCOPE_INVALID — The job exists and belongs to another workspace.
401AUTH_REQUIRED — No credential was presented, or the bearer token is not a key TAVONEL issued. This is what an unauthenticated request to any scoped route returns.
404COMPILE_JOB_NOT_FOUND — No such job in this workspace. Job ids are workspace-scoped, so this is also the answer for another tenant's id.
503COMPILE_JOB_STORE_READ_FAILED — The durable job store could not be read.

Resuming after a disconnect

Resume after a disconnect
curl -N https://tavonel.com/api/compile-jobs/<jobId>/events \
  -H "Authorization: Bearer $TAVONEL_API_KEY" \
  -H "Last-Event-ID: 42"

Reconnecting is the normal case

Note The server closes the stream on its own clock. Reconnecting is the normal case, not an error path — every frame carries the sequence to resume from.

The other stream: observed runs

A compile has its own transitions, above. A connector sync or an intake is an observed run, with its own append-only event ledger and its own stream. Both resume the same way, from Last-Event-ID; this one also accepts an after query parameter for clients that cannot set the header, and sends a bounded heartbeat so an intermediary does not close an idle connection.

GET/runs/{runId}/eventsScope documents:read

Replays append-only observed run events after Last-Event-ID, then streams new events and a bounded heartbeat. This is the second of two event streams and the one over *observed* runs — a connector sync, an intake — where /compile-jobs/{jobId}/events follows a compile's own transitions. `after` is the query-parameter form of Last-Event-ID, for clients that cannot set the header.

curl -sS -X GET https://tavonel.com/api/v1/runs/{runId}/events \
  -H "Authorization: Bearer $TAVONEL_API_KEY"
StatusResponse
200Persisted run-event stream. Frames carry the durable sequence as id; a heartbeat frame keeps intermediaries from closing an idle connection.
400RUN_ID_REQUIRED — The run id path segment was empty. JOB_CURSOR_INVALID — The run's cursor did not validate.
401AUTH_REQUIRED — No credential was presented, or the bearer token is not a key TAVONEL issued. This is what an unauthenticated request to any scoped route returns.
404JOB_NOT_FOUND — No such run in this workspace. JOB_SCOPE_INVALID — The run exists and belongs to another workspace. Answered as not found rather than as forbidden, so an id cannot be probed across tenants.
503JOB_STORE_READ_FAILED — The run store could not be read.

Change notifications

A stream needs a client holding it open. A change notification does not: when a finished compile's candidate differs from the World your AI currently reads, TAVONEL sends one signed POST to the endpoint registered for that workspace. Processing is automatic; activation is not. The candidate waits for a person to approve it in the workspace, and a notification activates nothing.

Note Before you start. Notifications are set up per workspace with us, like intake: we register your endpoint and give you that workspace's signing key, and until then nothing is sent. The endpoint must be https on the default port and must answer without a redirect. There are no email notifications.

FieldMeaning
typeAlways world.candidate_awaiting_activation.
deliveryIdThe same on every retry of one notification, and repeated in the tavonel-delivery-id header.
workspaceKeyThe workspace the change belongs to. A workspace is only ever sent its own changes, signed with its own key.
collectionIdThe World that changed.
candidateManifestDigestThe candidate waiting for approval: its manifest digest, written sha256: followed by 64 hex characters.
activeManifestDigestThe version your AI reads now, in the same format, or null when nothing has been activated yet.
processedAtWhen the compile reached the state that produced the candidate.
reviewRequiredTrue when the compile stopped for a person's review rather than finishing ready.
activationAlways explicit_human_decision: a person activates the candidate in the workspace, or nothing changes.

Verifying a notification

Every request carries three headers: tavonel-delivery-id, tavonel-timestamp (Unix seconds) and tavonel-signature, which is v1= followed by a hex HMAC-SHA256. The signed string is the delivery id, the timestamp and the raw body, joined by dots. Verify the raw bytes before you parse them, refuse a timestamp more than 300 seconds from your own clock, and compare in constant time.

Verify a change notification — Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// signingKey: the key we gave you for this workspace.
// rawBody: the request body exactly as received, before any JSON parsing.
export function verifyTavonelNotification(headers: Headers, rawBody: string, signingKey: string) {
  const id = headers.get("tavonel-delivery-id");
  const timestamp = headers.get("tavonel-timestamp");
  const signature = headers.get("tavonel-signature");
  if (!id || !timestamp || !signature) return null;
  if (!(Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300)) return null;
  const expected = Buffer.from("v1=" + createHmac("sha256", signingKey)
    .update(id + "." + timestamp + "." + rawBody)
    .digest("hex"));
  const given = Buffer.from(signature);
  if (given.length !== expected.length || !timingSafeEqual(given, expected)) return null;
  const event = JSON.parse(rawBody);
  return event.deliveryId === id ? event : null;
}

// Delivery is at least once. Keep the delivery ids you have handled (in your database, not
// in memory) and answer 2xx to one you have already seen without acting on it again.

Retries and duplicates

Delivery is at least once. A response other than 2xx, a timeout or a redirect counts as a failure, and the notification is sent again with the same delivery id and the same body: up to six attempts, with growing delays between them, the longest six hours. After the sixth failure it is recorded on our side as undelivered and not sent again; the candidate is unaffected and still waits in the workspace. Because a retry can follow a delivery you did process, keep the delivery ids you have handled and answer 2xx to a repeat without acting on it twice. Answer 2xx only once the notification is stored, and keep the response short. A response body over 4 KiB counts as a failure, whatever its status.

API version 2026-09-02.1 · reviewed 11 September 2026

Something here out of date or wrong? Report an issue with this page.