1.0.0 — 2026-07-25
First stable release. The public API and wire contract are now frozen for the 1.x series.
Changed
- The public API is frozen and machine-checked. Public functions now have
documentation and typespecs, with a regression test enforcing both.
Belay.get_job/2consistently returns{:error, :not_found}for a missing job;await_result/3propagates that result immediately. - The wire contract is stable. Schema migration revision 3 introduces
belay_meta.contract_major, separating protocol compatibility from additive migration bookkeeping. PostgreSQL storage checks compatibility at startup and emits an actionable failure before producers start. - Migrations are deploy-safe.
migrate!/1now applies all pending revisions atomically under a transaction-scoped advisory lock, so multiple nodes may invoke it concurrently. - PostgreSQL support is explicit. PostgreSQL 14–18 are supported; CI runs the PostgreSQL suite on 14, 16, and 18 across the Elixir/OTP compatibility matrix.
- Package verification now tests the artifact users receive. CI unpacks the Hex tarball, compiles it, and builds its docs. The formal-verification overview required by ExDoc is included without shipping the nested verification project.
Queue claims now emit
[:belay, :queue, :claim, :start | :stop | :exception]telemetry spans with queue, demand, and claim-count metadata.- Documentation now consistently names
steer_job/3and reports the current suite counts.
1.0.0-rc.6 — 2026-07-23
Changed
- Breaking (API naming). Two job-control functions gained the
_jobsuffix so the family is consistent and matches Oban.Belay.cancel_job/2andBelay.steer_job/3replace the formercancelandsteer. Update call sites; behaviour is unchanged. This is a deliberate pre-1.0 rename to freeze a consistent surface before the stable release.
1.0.0-rc.5 — 2026-07-21
Launch-prep release: formal verification layer, chunk workers, adaptive concurrency, the Oban migrator, dashboard v2, and the fixes surfaced by a pre-launch adversarial review.
Added
Formal verification layer (
verify/). A TLA+ model of the durable-execution core (journaling, budgets, cooperative cancel, crash/reclaim, settlement), generated from source + execution traces and exhaustively checked by TLC — 187,975,659 distinct states (depth 49), zero violations. Validated by mutation: reverting any of the three known production bugs in the model (the budget crash window; the cancel-request clear; the operator-retry semantics) yields a TLC counterexample matching the real-world failure step for step.Schedule-explored wake protocol (
verify/wake_protocol/). The parent-wake protocol under Lockstep controlled concurrency: the pre-fix count-gated protocol loses the wake on a found, saved, replayable schedule (PCT iteration 1); the shipped unconditional protocol survives every explored interleaving without the sweeper reconciler — evidence the reconciler is a backstop, not load-bearing.SCHEMA.md— the wire contract. The Postgres schema specified as a versioned protocol: tables, state machine, annotated SQL for every operation, advisory-lock discipline, the two soak-learned race rules, and a conformance path (the soak driver verifies any foreign worker SDK by reading the database). Groundwork for Python/TypeScript SDKs as thin contract implementations rather than rewrites.Belay.Codec— cross-language value envelope. Step values and job results now decode as Erlang term format (leading byte 131, written by Elixir) or UTF-8 JSON (written by any other SDK); tested with foreign-written JSON rows replaying through the engine.Chunk workers.
chunk: [size: n, gather_ms: t]+run_chunk/1: the producer gathers claimed jobs per worker and runs them as one invocation — one bulk INSERT instead of hundreds, one batch-priced embeddings call instead of a hundred singles. Per-job outcome maps retry only the failed jobs; gathered jobs are already leased, so crashes mid-gather reclaim cleanly. Full chunks dispatch without waiting for the gather window.Adaptive per-queue concurrency.
limit: [min: a, max: b]scales each node's limit up while claim rounds come back saturated and decays it when the queue idles — leaderless, like the burst-poll cadence it mirrors, and exactly bounded byglobal_limit/rate/partition limits. Policy-driven scaling stays a recipe: drive runtimeQueues.put/3from application code.Dashboard v2. KPI strip with live throughput and spend-rate sparklines (trailing-window snapshots persisted client-side), queue rows with limit-utilization bars (adaptive ranges shown as min–max), a two-line job list with args previews, durations, per-job cost badges and state-chip filters, and a restyled journal-timeline drawer. The spend KPI rides the same single stats scan the counts already paid for (
queue_statsnow returns spend sums); job summaries gainedmax_attempts,input_preview,spent_usd_micros,started_at.?sse=0renders a static snapshot for screenshot tooling.mix belay.migrate_oban— move an Oban installation's pending work in one command: dry-run analyzer (state census, per-worker port verification,executing-row warnings), faithful conversion (schedules, retry counts, errors, priority),--maprenames, and idempotent re-runs viameta.migrated_from_oban_id. History stays put by design — the guide documents the archive pattern instead.
Fixed
- Dashboard and MCP mutations now fail closed. A tokenless dashboard and the MCP server are read-only unless an authorizer or explicit mutation opt-in is configured. Dashboard writes reject query-string credentials, cross-origin requests, non-object/malformed JSON, bodies over 1 MiB, large header sets, and ambiguous request framing. Bearer-token comparison uses a constant-time digest check.
- PostgreSQL URLs now match deployment reality. Percent-encoded
credentials/database names, Unix-socket hosts, connection query options,
and secure
sslmodevalues are parsed and tested; unsupported ambiguous TLS modes fail with a configuration error instead of silently weakening a connection. Explicit child-spec options retain precedence over URL values. - Queue pause/resume is synchronous. The API now returns only after the producer has changed state, closing the race where a caller could pause a queue and immediately observe one more claim.
- Zombie acknowledgements have a dedicated proof and dual-adapter
regression. A stale attempt cannot commit after lease expiry and reclaim;
the focused TLA+ model completely checks the fence, while its no-fence
mutant produces the expected
Claim(1) → Expire(1) → Claim(2) → Ack(1)counterexample. - Operator retry now clears a pending cancel and respects workflow
dependencies. Found by extending the TLA+ model with a
Retryaction: (1) retry leftcancel_requestedset, so a cooperatively-cancelled job was un-retryable forever — ready, claimed, instantly re-cancelled, in a loop — and a retry racing a stale cancel was silently defeated; (2) retry sent workflow members straight toready, so a cascade-cancelled dependent could run (and succeed) while its dependency sat failed. Retry now clears the flag (the operator's later intent wins) and re-holds workflow members through a settlement pass under the workflow lock — released if deps are satisfied, re-doomed if not. Both adapters, both confirmed by failing tests against the real engine before the fix. - Budgets are now enforced before every step execution, not only after. The 7-hour endurance soak (99,004 jobs, 4,978 worker kills, 13 Postgres restarts) caught 6 jobs paying for one step past their budget: the check ran only after journaling a new step, so a crash between journaling the over-budget step and acking the failure let the next attempt replay past the journal and execute one more paid step. The runner now pre-flights the budget against durable spend before running any step body (no extra queries — it reuses the row the cancel check already fetches). A deterministic regression test pins the crash window.
- Cancel requests now survive crashes and retries. The adapter-
equivalence property test (below) caught the Postgres ack path clearing
cancel_requestedon reclaim/retry while Memory preserved it — silently losing a user's cancellation if the worker died before honoring it. The flag now persists across all non-terminal transitions on both adapters; the wire contract states it normatively.
1.0.0-rc.4 — 2026-07-20
The "better at every dimension" release: the embedded dashboard, transactional enqueue, runtime CRUD, encryption, and exact partitioned claims.
Added
Belay.Dashboard— an embedded web dashboard with zero dependencies (hand-rolled HTTP overgen_tcp, single-file UI, SSE live updates): queue tiles with limits and live counts, filterable job list, a journal drawer (steps with costs, events, errors, children), a rendered workflow DAG, and retry/cancel/signal/steer actions. Tokenless dashboards are read-only; writes require a token or the same pluggable authorizer as the MCP server.Belay.Txn— transactional enqueue inside your own Postgrex or Ecto transaction (duck-typed overquery!; still no Ecto dependency). With the:postgresnotifier, the wake-up is issued viapg_notifyinside the transaction, so it delivers exactly on commit and never on rollback.- Runtime queue and cron CRUD —
Belay.Queues.put/delete/listandBelay.Crons.put/delete/pause/resume/list, persisted in the database, validated eagerly, reconciled by every node'sQueueSync(producers now live under a DynamicSupervisor); dynamic entries override static config by name. Leaderless, like everything else. - Encrypted inputs —
use Belay.Worker, encrypted: trueplusencryption: [key: {mod, fun, args}]: AES-256-GCM envelopes at rest, plaintext only inside the executing process; schemas validate before encryption; replay decrypts transparently. - Shared view serializers (lib/belay/view.ex) so the dashboard and MCP describe jobs identically.
bench/throughput.exs— measured ~416 trivial jobs/s end-to-end on a laptop (3 worker processes, unbatched acks).
Changed
- Partitioned claims are now exact. Per-key allowances are computed with a window-function ranking inside the claim transaction (under the queue advisory lock), replacing the bounded over-fetch heuristic — heavy key skew can no longer starve minority keys (regression-tested with 30:1 skew).
1.0.0-rc.3 — 2026-07-20
Dispatch-latency release: agent workloads are bursts of short tasks, so
insert→result overhead is the product. Measured on stock settings
(bench/run.sh): ~9ms p50 same-node, ~11ms p50 / ~25ms p99 across
unconnected OS processes with the new notifier, ~50ms p50 on adaptive
polling alone (vs ~250ms average before).
Added
- Notifier layer (
Belay.Notifier): wake-ups as accelerators, never load-bearing.:local(registry +:pg, always on) and opt-in:postgres—pg_notifypokes and result notifications across fleets that share Postgres but not an Erlang cluster, with a dedicated auto-reconnecting listen connection per node and channel names scoped per database. If the channel is down, latency falls back to the polling floor; correctness never depends on NOTIFY. - Adaptive burst polling: producers poll at
busy_poll(default 25ms) while claiming work and decay exponentially topoll_intervalwhen idle; poke storms coalesce into single claim rounds. - Fast
await_result: wakes on result notifications; otherwise re-checks on a 5ms→200ms backoff instead of a fixed 200ms. - MCP authorizer hook:
mix belay.mcp --authorizer MyGuard(orauthorizer:onBelay.MCP.serve/2) gates the mutating tools (retry/cancel/signal/steer) behindauthorize(tool, args)— the mount point for capability-token systems (e.g. Legant) supervising operating agents. bench/— reproducible latency benchmark across the three topologies.
Fixed
- Soak/bench harnesses now clean up respawned workers by pattern (stray workers were exhausting Postgres connections).
1.0.0-rc.2 — 2026-07-20
Hardening release driven by the chaos soak harness (soak/), which runs a
mixed workload across multiple worker OS processes under kill -9 and full
Postgres restarts, then verifies thirteen invariants. The passing report
lives in soak/REPORT.md.
Fixed
- Parent wake-up races in dynamic children. Under READ COMMITTED, the
last two children acking concurrently could each see the other as
incomplete and both skip the parent's
$childrensignal, parking the parent forever. Fixed in layers: children now signal unconditionally on every terminal ack; signal delivery and awaiting-parking serialize on a per-scope advisory lock; and the sweeper re-readies any parent awaiting$childrenwhose children are all terminal — so even an unknown residual ordering degrades to a sweep-interval delay, loudly logged, never a stuck job. - Database-outage resilience. Producers, the lease keeper, the sweeper, and the cron scheduler now rescue storage failures and skip the cycle with a warning instead of crash-looping the supervision tree; restart budgets are lenient. A full Postgres restart mid-load is survived with claim rounds skipped and no losses.
- Spawn idempotency across the insert/journal gap. Children carry always-scoped unique keys derived from (parent, spawn name, index), and the id list is rebuilt from those keys — a crash between inserting children and recording the spawn step can no longer duplicate them.
Added
soak/chaos harness (workers, driver with ledgered expectations, kill/restart orchestration, invariant verification, report generation).- Third uniqueness scope:
unique: [key: k, scope: :always]. - Sub-second worker timeouts:
timeout: {n, :millisecond}.
1.0.0-rc.1 — 2026-07-20
First release candidate. Everything below ships open, Apache-2.0.
Engine
- Claim/lease/ack execution with attempt fencing; expired leases reclaimed cluster-wide in seconds, stale acks rejected.
- One-rule scheduling: a job is claimable when
ready_atis due — scheduled work, retry backoff, and snoozes share it; there is no staging step and no leader election anywhere. - Poll-first dispatch with in-cluster pokes; no LISTEN/NOTIFY dependency.
- Graceful shutdown: producers stop claiming first; running jobs get
shutdown_grace; the cluster reclaims the rest. - Per-worker execution
timeout:; queuepause/resumeat runtime. - Constraint-backed unique jobs:
unique: "key"(while incomplete) andunique: [key: k, within: seconds](per window); duplicates return the existing job flaggedduplicate?: true. - Retention pruning per terminal state, cascading steps and events; signal TTLs; rate-window cleanup.
- Leaderless cron with per-slot dedup via a unique index.
- Admission control per queue:
global_limit(live-leased counting), sliding-windowratelimits, per-keypartitionfairness.
The agent layer
- Durable steps (
Belay.step/4): memoized per job with cost columns — retries replay past completed work. - Budgets (
budget: [usd:, tokens:]): jobs fail with:budget_exceededthe moment accumulated step costs cross the cap. - Signals (
await/3,signal_job/4): park at zero cost, wake instantly, deadline timeouts; steering (steer/3→steering/1) injects guidance into running jobs; cooperative cancellation at step boundaries. - Dynamic children:
spawn/3andspawn_many/3(replay-safe, memoized),await_children/1,map_children/5fan-out/fan-in — agents grow their own DAGs at runtime. - Workflows: DAG dependencies in the
heldstate, released and cascaded transactionally inside the completing job's ack;ignore:policies. - Batches: transparent sugar over workflows with an any-outcome
on_completecallback. - Durable event streams:
emit/2+ live subscriptions + offset replay. - Replay debugging:
Belay.Replay.dry_run/2re-runs job code against the recorded journal, side-effect-free, with precise divergence reports. - Token-resource rate limits: shared resource buckets with estimated
admission and post-hoc true-up via
debit/3. - Input schemas: insert-time validation raising at the call site.
Operability
stats/1,list_jobs/2,retry_job/2,steps/2,events/3.- Telemetry spans with durations;
Belay.Telemetry.attach_default_logger/1. - MCP server (
mix belay.mcp): stats/jobs/steps/events introspection plus retry/cancel/signal/steer, over stdio JSON-RPC.
Storage
- Behaviour with two adapters: deterministic in-memory (test/simulation reference) and Postgres (Postgrex, hand-written SQL, clock passed as a parameter). The same 64-test suite runs against both.