Lanes and statuses
There are four lanes:chat, collect, steer, follow_up
There are six job statuses:
queued, leased, running, completed, failed, canceled
Two narrower vocabularies sit alongside them: queue commands are typed steer, follow_up or abort, and the queue-command lanes are steer and follow_up only.
Lifecycle receipts
A job’s state is a fold over these ten event types on the streamjobs/<jobId>:
job.enqueued, job.leased, job.heartbeat, job.progress, job.completed, job.failed, job.canceled, queue.command, queue.command.consumed, job.lease_expired
The reducer enforces three invariants worth designing around:
job.leasedis ignored unless the job is currentlyqueued. A second lease on an already-leased job changes nothing.job.heartbeatpromotesleasedtorunning, but only when the heartbeat’s worker id and attempt match the active lease. A heartbeat from a superseded attempt is inert.job.progress,job.completedandjob.failedapply the same active-lease check.- An event for a job that does not exist throws
Invariant: no job <jobId> for <type>. There are two exceptions:job.failedandjob.lease_expiredare ignored rather than fatal, so a late settlement from a job that was never appended cannot break a fold.
Singleton semantics and enqueue idempotency
singletonMode defaults to allow. Its other two values act on a sessionKey:
cancel— every active job on that session key (queued,leasedorrunning) is stopped with the reasonsingleton cancelbefore the new job is written. Aqueuedjob is canceled outright withjob.canceled; aleasedorrunningone receives aqueue.commandof typeabortcarrying that reason, because only the worker can stop work already in flight.steer— the most recent active job on the session key receives aqueue.commandof typesteercarrying the new payload plusfromSessionKeyandfromEnqueue: true, and that existing job is returned instead of a new one being created.
running job cannot swallow a steer.
The idempotency contract. An explicit
jobId that already exists returns the existing job unchanged. Re-posting the same enqueue with the same jobId never creates a second job and never disturbs the first. This is the property callers rely on to make an enqueue safely retryable.maxAttempts defaults to 2 and is clamped to the range 1..8.
Payload kinds
Nine payload kinds are accepted:factory.dispatch, factory.run, factory.integration.publish, factory.integration.validate, factory.objective.audit, factory.objective.control, factory.objective.watchdog, factory.task.monitor, factory.task.run
Every one of these except factory.objective.watchdog requires an authContext carrying a userId and an organizationId (with optional workspaceId, receiptConnectGatewayUrl, sessionId and source).
Each kind also declares a contract: a workerGroup of chat, codex or control, an optional durable workflow, and flags for durable activity, objective scoping, objective reconciliation on a terminal or expired lease, live execution and terminal objective audit. Only factory.task.run sets hasDurableActivity.
Actual Resonate routing, however, is decided agent-id first and kind second:
These two do not always agree. A
factory.dispatch job posted to agent id factory lands on the chat group even though its kind contract names control as its worker group. Treat this as an ambiguity in the implementation rather than assuming either one is the contract.The trap: only four agent ids have handlers
Exactly four agent ids have registered job handlers:factory, factory-control, factory-monitor, codex
The payload is checked even though the agent id is not: a body whose kind is not one of the nine payload kinds above is rejected before the job is written, and the request returns 500 Server error.
Who executes a job
A job runs on whichever process registered the worker function for its group, so nothing executes unless the deployment actually runs those roles; how many jobs each one takes at a time is per role, andRECEIPT_RESONATE_EXECUTE_CONCURRENCY overrides every default. See processes, roles, and routing for the full table.
The durable execution path
Step by step:- Enqueue.
job.enqueuedis appended tojobs/<jobId>. The caller gets back a 202 carrying the job object and anasyncblock with the job id, the stream name, the job and receipt event URLs, and the status. - Dispatch outbox. The queue receipt is the outbox. Enqueue never calls Resonate for the job it just created; it wakes a scanner pass, and that pass asks the driver starter to begin a driver RPC. Startup redrive, interval redrive and enqueue wake-ups therefore share one delivery path.
- Driver RPC. The base dispatch key is the job id. If a previous driver or execute promise exists and is terminal, the key advances a delivery generation; that is bounded at 32, after which it errors with
resonate delivery recovery exceeded 32 generations for <jobId>. If a promise is still pending, the starter returnscreated: falsewith the reasonactive_deliveryand does nothing. - Driver executes. The driver reads the job. A stale active lease ends the pass: the job is failed with
stale active Resonate job lost execution; retrying through Resonate driverso the ordinary redrive path can pick it up again. Otherwise it honoursabortRequestedand begins the worker RPC under a fresh attempt fence with the id<jobId>:attempt:<n>. The driver deliberately does not lease the job — leasing belongs to the worker. - Worker executes and settles. It leases if the job is
queued; work that was redelivered rather than leased here instead takes a start-fence heartbeat, and a heartbeat that does not match the current fence ends the attempt aslease_lost. It then checksabortRequested, races execution against the heartbeat loop and the execution timeout, re-reads the job, and completes, fails or cancels it under an attempt fence. Every settlement is wrapped in a storage retry: 5 attempts, 150 ms times attempt backoff, transient storage errors only. - Callback. The driver posts a JSON callback on each of
dispatched,completed,failedandcanceled, toRECEIPT_RESONATE_CALLBACK_URL/RECEIPT_EVENT_CALLBACK_URL, with a 5 s timeout and an optionalAuthorization: Bearerheader fromRECEIPT_CALLBACK_TOKEN.
Workers hold a long poll open against Resonate. That poll is issued with the request timeout disabled and reconnects with a backoff that starts at 1 s and doubles to a 30 s ceiling, so an idle worker keeps its delivery connection instead of dropping it between jobs.
Failure statuses in a result
When an attempt does not complete, the worker’s execution result carries one of these statuses:settlement_conflict, stale_attempt, lease_lost, terminal_state, canceled, execution_timeout, failed
A failure that is marked non-retryable copies its status onto job.result in the job.failed receipt. A retryable one does not: the receipt records the error and requeues the job, and the status stays in the worker’s own return value.
Leases, timeouts and heartbeats
The execution timeout is the lease minus 1 000 ms, with a floor of 5 000 ms.
FACTORY_CONTROL_JOB_EXECUTION_TIMEOUT_MS may lower it, for objective control only.
The heartbeat cadence is derived rather than configured outright: it is one third of the smaller of the lease and RECEIPT_RESONATE_ACTIVE_STALE_MS (default 600 000 ms), floored at 1 000 ms. RECEIPT_RESONATE_HEARTBEAT_INTERVAL_MS can only lower that derived value, never raise it. Dividing by three gives a worker two renewal opportunities before the fence fires.
The driver’s own invocation timeout is the lease plus 60 000 ms, with a floor of 120 000 ms.
Four independent recovery loops
The queued-job redrive is guarded by a minimum age (
RECEIPT_RESONATE_QUEUED_REDRIVE_MIN_AGE_MS, 30 000 ms) and a cooldown (RECEIPT_RESONATE_QUEUED_REDRIVE_COOLDOWN_MS, 15 000 ms), and its dispatch key is <jobId>:redrive:<attempt>:<updatedAt> so a scanner pass is idempotent for one observed queue state.
The outbox redrive is single-flight. RECEIPT_OBJECTIVE_CONTROL_OUTBOX_REDRIVE_TIMEOUT_MS (default 15 000 ms) is an observational deadline only: a pass that runs longer keeps ownership until it settles and logs factory.control_outbox_redrive_slow, so interval ticks coalesce instead of piling duplicate database work on top of a slow pass.
The watchdog reconciles an objective for one of three reasons — no_active_objective_work, phase_supersession_has_active_stale_job or active_objective_work_stalled — by enqueuing an objective-control job with the reason reconcile. Sandboxed execution is treated as stale only after 15 minutes, deliberately longer than the UI’s freshness window, because a computer-backed task can be quiet while its workspace syncs. The watchdog can be turned off with RECEIPT_FACTORY_OBJECTIVE_WATCHDOG_ENABLED=0; its timeout defaults to 60 000 ms with a 10 000 ms floor, and its scan limit to 200, clamped to 1..2000.
The durable ledger is beside receipts, not above them
A Postgres-backed workflow and activity ledger tracks durable execution alongside the receipt chains. Every durable side effect is best-effort: a durable write failure is logged and does not fail the receipt mutation that has already been appended. Queue commands are a deliberate straight pass-through, so steer and follow-up never depend on workflow signal delivery. The same rule governs Resonate itself. It is a durable delivery mechanism, not ownership state — the receipt chain is the authoritative queue. When delivery is lost after a job was leased, the queue appends a receipt-leveljob.lease_expired event so the ordinary redrive path can recover it.
Next step: see how Factory turns an objective into these jobs.