Files
alkstore/docs/architecture/core-contract.md
T
glm-5.3-flash 029f885aa1 general review 004 (pre-release, whole workspace): one HIGH slot-poisoning fix, two contract-observable divergence fixes, robustness + doc corrections — all gates re-run green
- sqlite scheduler tick: a failed COMMIT (or ROLLBACK) left the connection
  released with a stuck-open transaction, wedging the writer slot forever —
  the one commit site missing the drop-+-reopen-replenish arm; tick now
  takes the conn by value with a poisoned disposition (drop + replenish),
  the commit-fault seam reaches the tick, and
  failed_tick_commit_replenishes_the_writer_slot pins the arm
- pg save_offset_tx stored GREATEST($3,0) where the wave-8 gate's
  negative-seed fix (raw seed, ADR-023 §2 2026-10-11 annotation) covered
  only the auto-commit form; mem's merged in-tx read clamped a staged
  negative seed against a phantom zero; both fixed and the tx-form
  totality pinned by a new stamped suite leg (in_tx_reads_see_own_writes)
  green on all three columns vs the live harness server
- pg: a failed forwarder register leaked the channel refcount forever
  (no receiver to drop it) — listen/spawn_bridge now unregister on the
  register error, mirroring the subscribe-failure arm; register doc
  corrected to put the failure disposition at the call site
- pg forwarder: nine poisoned-mutex expect() panics converted to the
  mem crate's into_inner recovery idiom (no-expect-outside-tests rule)
- core encode_payload: a deeply-nested Value aborted the process in
  serde_json's unguarded serializer recursion and the fuzz pin blessed
  storing bytes no decoder could read back; encode now rejects nesting
  past 127 containers (the empirically-verified decode limit) with typed
  Codec — decode-aligned boundary pins added to the fuzz invariant and
  value_type_tests, applied pre-publish within the open class-1 window
- scheduler sleep deadlines: Instant + Duration overflow caps (MAX_SLEEP_S,
  tick-and-resleep) on all three engines for far-future boundaries;
  sqlite tick claim stamp now saturating_add
- docs read true again: Error::Closed scope (stream try_recv arm),
  validation whitespace wording, sqlite lib.rs phantom durability knobs,
  pg lock.rs honest autocommit wording, mem tx/stream merge + wake-
  granularity wording; wakes.rs stale allow(dead_code) removed
- tests: value_type_tests exercised its own map_err instead of the
  function under test (fixed); unwrap_or_default reference encodes
  panic with cause
- docs/reviews/004-pre-release-general-review.md records nine fixed +
  sixteen parked findings (sqlite seam panic-safety armor, watcher
  post-death-subscribe hang + initial-stat self-kill, pg forwarder
  TOCTOU/hang/diagnosability, pg BIGINT server-side stamp overflow, mem
  growth/outbox notes, pg identifier-truncation doc note, smaller) and
  two false positives caught on verification; implementation.md
  review-rounds entry + core-contract backlog rows updated
- gates: build, workspace tests (sqlite 194, core 27, suite legs new),
  full live pg column 127+25+9, clippy -D warnings, fmt, fuzz replay
  6/6, wasm32 check, doc gate -D warnings — all green
2026-10-11 09:25:59 +00:00

889 lines
51 KiB
Markdown

---
status: stable
last_updated: "2026-10-11 (the pre-release class-1 close-out pass — the
publish→wake ordering tolerance recorded; ADR-023 §2's
`heartbeat(extend)`/`save_offset` classifications;
`EventReceiver::try_recv`'s closed shape; `get_job`'s id-global prose —
then wave-8 gate: flipped stable with the close-out pass's suite legs
landed; the contract text freezes at the gate, ADR-017 §2)"
---
# Core contract
The unified, engine-agnostic surface a `Store` exposes. This document
specifies WHAT the contract is; the per-engine specs map it onto their
machinery; ADRs carry the WHY. The starting artifact is the honker-rs
surface (the Phase 0 §Interface finding) scoped to the
inventory-confirmed features — [ADR-002](decisions/002-feature-scope.md)
— and pinned against it. The base surface is pinned by
[ADR-008](decisions/008-contract-v1-pinning.md) (contract v1 partition,
`TxHandle` representation, wake type, reserved strings, error
taxonomy, config split — amended in place, pre-implementation, by
[ADR-014](decisions/014-outbox-tx-enqueue.md): `outbox_enqueue_tx`
joins the `TxHandle` trait, and by
[ADR-015](decisions/015-streams-depth.md): streams depth — key
semantics, `StreamEvent` shape, the ordering row, `trim_to`,
and `publish_with_key_tx`); ADR-009/ADR-010 add the first extensions
beyond ADR-008's original v1 partition (scheduler collapse surface,
`QueueOpts` depth — factually pre-release additions per ADR-017's
class 1, shipped inside the initial contract v1 text; versioning
discipline for the surface's changes is
[ADR-017](decisions/017-contract-versioning.md)'s, resolved: the
core crate's semver is the contract version);
[ADR-016](decisions/016-deployment-honesty.md) decides the parked
capability-surface question: **none, by default ever** — engine
differences surface at compile time (engine-crate identity) and in
the [deployment matrix](deployment.md), never as a runtime
descriptor; the
*obligations* are this document.
## Concepts
- **Store** — what a consumer opens from a connection string or file
path ([ADR-001](decisions/001-crate-split.md)): one handle, the engine
behind it chosen at open time. Consumer code never branches on
engine type (guiding principle 4). (The mem engine is the one
documented exception to the open-prose: its constructor is
engine-native — `MemStore::new()`, no connection string — per
[ADR-024](decisions/024-mem-engine.md) §4; the engine choice
remains a dependency-graph fact made before any `Store` exists.)
- **Mechanism** — one of the surface's coordination families: notify,
streams, queues (+outbox), locks, scheduler. Delivery guarantees are
pinned per mechanism in [ADR-006](decisions/006-wake-and-delivery-contract.md)'s
table (notify/streams/queues), extended with the locks row by
[ADR-008](decisions/008-contract-v1-pinning.md), the scheduler row
and collapse by [ADR-009](decisions/009-scheduler-collapse.md).
*(Guiding principles are defined in `docs/research/phase-0.md`
§Vision and cited by number throughout this directory.)*
- **Wake** — the opaque "something changed, re-read" signal the wake
contract delivers ([ADR-006](decisions/006-wake-and-delivery-contract.md)).
The contract's central abstraction.
- **TxHandle** — the caller-held transaction object whose `*_tx`
methods make side effects commit-atomic with a business write
([ADR-007](decisions/007-transactional-seam.md)). Contract shape:
the `*_tx` methods live on the `TxHandle` trait itself — engines
implement it for their concrete handle, no downcast
([ADR-008](decisions/008-contract-v1-pinning.md) §2).
## The seam
Per [ADR-008](decisions/008-contract-v1-pinning.md) §6, the trait is
constructed by engine crates (durability knobs, pool sizing, watcher
cadence are engine options — [deployment.md](deployment.md) carries
the facts); the contract is the trait surface it returns:
```text
Store::begin_tx() -> Box<dyn TxHandle + Send>
trait TxHandle {
enqueue_tx(name, opts, payload) -> job_id
publish_tx(stream, payload) -> offset
publish_with_key_tx(stream, key, payload) -> offset
notify_tx(channel, payload)
save_offset_tx(stream, consumer, offset)
get_job_tx(queue, job_id) -> Option<Job> // ADR-021
get_offset_tx(stream, consumer) -> i64 // ADR-021
read_since_tx(stream, offset, limit) -> Vec<StreamEvent> // ADR-021
read_from_consumer_tx(stream, consumer, limit) -> Vec<StreamEvent> // ADR-021
outbox_enqueue_tx(outbox, opts, payload) -> job_id
commit(self: Box<Self>) -> Result<()> // or rollback
}
```
The tx read methods (`get_job_tx` / `get_offset_tx` / `read_since_tx`
/ `read_from_consumer_tx`,
[ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §1) make
read-your-own-writes possible inside the business transaction — they
take the mechanism name (mirroring `enqueue_tx`/`publish_tx`'s
name-taking shape), read the caller's own transaction's rows, and are
pure reads (no ops ride them). Reads and saves are tx-shaped;
claim/maintenance ops never are (no `claim_tx` — a claim's visibility
deadline must not be tied to a business transaction's lifetime;
deliberate absence, not an omission). **Dropping a handle without
`commit`/`rollback` rolls it back**
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §4) — the
no-ghosts property holds through
RAII paths, not only the explicit ones.
`outbox_enqueue_tx` takes the **outbox name** and derives the backing
queue engine-side (the reserved prefix makes the derived name
unreachable by `enqueue_tx` by design —
[ADR-014](decisions/014-outbox-tx-enqueue.md)).
`publish_with_key_tx` validates the key like the non-tx form (a
present key must be non-empty → `InvalidName`) and is
`publish_tx`'s keyed twin — commit-atomic keyed publishes are the
point ([ADR-015](decisions/015-streams-depth.md) §2).
Mechanism handles come off the store (or, for transactional variants,
off the handle). **Payload encoding** ([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
§4, signature typed by [ADR-023](decisions/023-fourth-review-round.md)
§1): the trait crosses `serde_json::Value`; engines serialize it with
serde_json via core's `encode_payload` (fallible — `Codec` on
serialization failure, typed at the helper; never a silent fallback
form) and store exactly those bytes — `payload_as<T>` decodes
them, `Codec` on failure; round-trip (publish → read → decode →
`Value` equality) is what the contract suite pins, on both engines.
**Numeric-argument domains** ([ADR-023](decisions/023-fourth-review-round.md)
§2) — classified by argument kind, uniform on both engines (no engine
may reinterpret a caller-supplied numeric argument via SQL-dialect
semantics; validation happens at the trait-impl entry point, before
the engine's storage layer):
- **Extents** ("at most this many": `claim_batch(n)`,
`read_since(limit)`, `read_from_consumer(limit)`) — non-positive
values yield the **empty value result** (a value, not an error —
the no-work vocabulary); the SQLite `LIMIT -1` dialect artifact
(a negative extent reaching the whole queue/stream) is dead by
this rule.
- **Durations** (`try_lock(ttl)`, `renew(ttl)`) — non-positive values
are rejections (`Err`, opaque `Database`, detail in the source
chain); an exclusion mechanism must not silently hand back a lease
the caller believes it holds.
- **Boundaries/offsets** (`trim_to(horizon)`, enqueue `delay`/
`run_at`/`expires`, `heartbeat(extend)`, `save_offset(offset)`) —
total: any `i64` is well-defined (a negative horizon deletes
nothing; a negative delay resolves to a ready time in the past,
i.e. ready now; a negative `heartbeat(extend)` — the renewed claim
deadline is `now + extend` — shortens or voids the claim, and the
validity predicate then refuses the ops, never an error; a negative
offset save is stored, reads resume forward, and saves at-or-below
the checkpoint are the monotone no-ops)
*(the two 2026-10-11 classifications annotated in
[ADR-023](decisions/023-fourth-review-round.md) §2 by the
close-out pass; before that date the table covered only the first
three arguments)*.
```text
store.notify(channel, payload) handle.notify_tx(channel, payload)
store.stream(name) -> StreamHandle handle.publish_tx / publish_with_key_tx
handle.save_offset_tx
(the StreamHandle also carries
trim_to — stream-side, outside the
tx seam)
store.queue(name, opts) -> Queue handle.enqueue_tx
store.outbox(name) -> Outbox handle.outbox_enqueue_tx(outbox, opts, payload)
store.try_lock(name, owner, ttl) -> Option<Lock>
store.listen(channel) -> Box<dyn WakeReceiver>
store.schedule(name, spec, queue, payload, opts) -> Result<Schedule>
store.unschedule(name) -> bool
store.run_schedules(stop: StopToken) -> Result<()>
```
The mechanism handles' full trait surfaces (`Queue { enqueue,
claim_one, claim_batch, ack_batch, cancel, get_job, sweep_expired }`,
`StreamHandle { publish, publish_with_key, read_since,
read_from_consumer, save_offset, get_offset, trim_to, subscribe }`,
`Outbox { enqueue, run_once }`, `Lock { renew, release }`, plus the
boxed `JobHandle` a claim returns) are pinned by
[ADR-019](decisions/019-mechanism-handle-surfaces.md); the
`StopToken` parameter is a core-crate type (same ADR, §4).
Payloads cross the trait as core value types (non-generic trait
methods — object safety of the boxed handles,
[ADR-008](decisions/008-contract-v1-pinning.md) §2); `payload_as<T>`
decodes on concrete returned values (`Job`, `StreamEvent`). The
`with_tx` closure wrapper ships over the handle shape (the POC-verified
composition direction, [ADR-007](decisions/007-transactional-seam.md);
signature pinned there 2026-10-07: `with_tx(f) -> Result<()>` with
`f: FnOnce(&mut dyn TxHandle) -> BoxedFuture<Result<()>>` — `Ok` ⇒
commit, `Err`/drop/panic ⇒ rollback; non-generic for object safety,
values return through captured state, no handle escapes the closure).
Mechanism handles (`Queue`, `StreamHandle`, `Outbox`, `Lock`) are
core-owned trait objects like the tx handle — engine types never appear
in consumer signatures. Their trait surfaces are pinned by
[ADR-019](decisions/019-mechanism-handle-surfaces.md) (placement
mirroring the honker-rs starting artifact: claim/maintenance ops on the
`Queue` handle, publish/read/trim on the `StreamHandle`, the claim ops
on the boxed `JobHandle`, `run_once` on the `Outbox`); trait-method
additions on these traits post-release ride
[ADR-017](decisions/017-contract-versioning.md) class 2 like any
other trait surface.
## Mechanism contracts
### notify / listen
Fire-and-forget signals, commit-atomic when sent in a transaction
([ADR-007](decisions/007-transactional-seam.md)). No durability, no
replay, no per-listener retry; a listener attached after a commit
never sees it.
- `notify(channel, payload)` — payload ≤ 8000 bytes on Postgres
(client-side checked, typed `PayloadTooLarge` before the round
trip, verified by POC #2; the measured quantity is the serde_json
serialization of the payload `Value` — ADR-020 §4's stored-bytes
form, so check and storage measure one thing); no limit on SQLite.
The error variant is
contract-wide (callers match it identically on both engines), the
*occurrence* is the documented engine asymmetry
([ADR-008](decisions/008-contract-v1-pinning.md) §5) — with no
capability surface
([ADR-016](decisions/016-deployment-honesty.md)), this variant is
the one runtime carriage of an engine asymmetry; its occurrence
asymmetry is pinned in the verification backlog below.
- `listen(channel) -> Box<dyn WakeReceiver>` — starts from "now";
delivers opaque `Wake { channel }` signals
([ADR-006](decisions/006-wake-and-delivery-contract.md)), never
payloads or ids ([ADR-008](decisions/008-contract-v1-pinning.md) §3).
Wakes are **best-effort hints, not per-commit delivery promises**:
possibly coalesced (SQLite) or per-notify (Postgres), possibly
repeated after reconnect, possibly absent entirely (SQLite burst
coalescing; the pg no-replay hole) — recovery is the consumer's
re-read plus the engine's reconnect surface, never a guaranteed
deliver. **Consumers must be idempotent on wake.**
- Failure surfaces as channel events, never silence: watcher death
closes the receiver (`recv() -> None`, SQLite); the synthetic
reconnect-wake (a reserved channel
([ADR-004](decisions/004-postgres-driver.md))) covers Postgres
connection gaps.
- Channel name is the one piece of semantic content a wake carries —
the invalidation key for caching subscribers.
### streams
Durable pub/sub with per-consumer offsets
([ADR-006](decisions/006-wake-and-delivery-contract.md)). The durable
cousin of notify: publish is commit-atomic; every committed event is
readable by every consumer whose offset hasn't passed it;
replay-on-attach is the default; offsets are explicit and
transaction-aware (`save_offset_tx` gives exactly-once-within-a-
business-tx shape). Depth pinned by
[ADR-015](decisions/015-streams-depth.md).
- `publish` / `publish_tx` / `publish_with_key` / `publish_with_key_tx`
— append to the stream's durable log; return the assigned offset.
**Key semantics ([ADR-015](decisions/015-streams-depth.md) §1): the
key is carried metadata** — stored on the event row, round-tripped
on every read, `Option<String>`-shaped on `StreamEvent` — with no
engine-enforced behavioral role: **the ordering guarantee stays
global FIFO by offset within the stream**; per-key in-order reading
is the documented emergent pattern (filter by key, read in offset
order), not a server promise. A present key must be non-empty
(`InvalidName`). Server-enforced per-key ordering re-enters only
via a consumer-inventory row naming it.
- **`StreamEvent` shape** ([ADR-015](decisions/015-streams-depth.md)
§3): `{ offset: i64, stream: String, key: Option<String>, payload:
Vec<u8>, created_at: i64 }` — `stream` renames honker's `topic`
(one term everywhere, the ADR-008 §4 kinds list); `offset` is
engine-assigned, monotone per stream, **immutable, never renumbered**
(gaps after a trim are legal); `created_at` is unix-seconds-at-
publish, informational, **not an ordering field** (offset is the
only one); `payload_as<T>` decodes (the error is `Codec`).
- **Ordering guarantee row** ([ADR-015](decisions/015-streams-depth.md)
§4, extending [ADR-006](decisions/006-wake-and-delivery-contract.md)
§2's streams row): `read_since`/`subscribe` yield `offset ASC` —
global FIFO per stream; committed-event visibility and
replay-on-attach as already pinned; offsets immutable. The
contract suite pins cross-engine equivalence against this row
(same publish sequence → same offset sequence → same read order).
- **Wake-fire ordering tolerance** *(documented tolerance, 2026-10-11
— the pre-release class-1 close-out pass,
[ADR-017](decisions/017-contract-versioning.md) §2; disposes the
wave-6 review gate's recorded observation)*: nothing in the
contract pins wake-fire order to offset order. Offset allocation
precedes insert+wake on a multi-threaded runtime (the mem engine;
its documented current-thread posture never exercises it) and the
offset is sequence-assigned pre-commit with the wake fired
post-commit (the Postgres engine — commit order can invert
sequence order), so two concurrent publishes can complete out of
offset-allocation order. A subscriber's wake-driven drain that
finishes between the two completions can checkpoint past the
later-completing, lower-offset event: **the event stays in the
log** — direct `read_since` always sees it, and a fresh
`subscribe(consumer)` whose saved checkpoint is below it consumes
it normally — but the live subscriber whose cursor passed it is
not re-delivered it by the later wake. This is a documented
consequence of the wake contract's best-effort posture (hints, not
delivery promises); the offset-ASC read guarantee above is
unaffected — reads always run over the committed log in offset
order regardless of wake timing. Consumers requiring per-event
completeness across concurrent same-stream publishers need the
defensive re-read recipe (a lagged checkpoint, or a direct
`read_since` sweep below it). Pinned as a tolerance, not an
enforced guarantee (the heavier wake = offset-order amendment was
weighed and declined with the user: enforcement against commit-
ordered visibility is a redesign, and the wake mechanism's posture
is hint-shaped by construction).
- `read_since` / `read_from_consumer(offset)` — cursor-based reads;
`get_offset(consumer)` — checkpoint inspection. A read from a
trimmed-away offset region (below) resumes at the trim horizon's
first remaining row.
- `save_offset` / `save_offset_tx` — consumer checkpoint, explicit;
**the contract's save is always explicit** (no auto-checkpoint
cadence — honker's per-binding ambiguity is not inherited). The
subscription handle exposes no `save_every` / auto-save-on-drop
([ADR-008](decisions/008-contract-v1-pinning.md) §8). Saves are
monotone — a save below the stored checkpoint is a silent no-op —
so the direct form and the receiver's no-arg form
(`EventReceiver::save_offset(&mut self)`, which saves the
last-yielded event's offset through the same op,
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §6)
interleave freely and cannot regress one another. Both save
forms' `Err` arm carries `Database` only
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §5) —
regression refusal is a silent no-op, so a save result errs only
on storage failure. A saved offset
below the trim horizon
([ADR-015](decisions/015-streams-depth.md) §5) stays a valid
position marker.
- `trim_to(horizon) -> count` — delete events with `offset <= horizon`
(exact-boundary; the horizon is a boundary argument, total per
[ADR-023](decisions/023-fourth-review-round.md) §2 — a negative
horizon deletes nothing, idempotently)
([ADR-015](decisions/015-streams-depth.md) §5; the return is the
deleted-row count, [ADR-019](decisions/019-mechanism-handle-surfaces.md)). The stream-side
bounded-growth op, consumer-invoked, no engine-default
retention and no ambient sweeper (the ADR-010 §6 posture; the
replay-forever default is the mechanism's purpose — growth is
documented with the tool in-contract to bound it). Trim emits no
dedicated wake and no notify — on SQLite the `data_version`
watcher may still fire (any committed write bumps it; a spurious
hint, contract-legal); surviving events keep their offsets.
- `subscribe(consumer) -> Box<dyn EventReceiver>` — durable
consumption: attach, read to current tail, resume after restart
from the stored offset; explicit `save_offset` on the receiver
(shape in [ADR-008](decisions/008-contract-v1-pinning.md) §8).
**Error and close arms pinned**
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §5):
`recv()`'s `Err` arm carries `Database` only (payloads cross raw;
`Codec` lives at `payload_as<T>`; `Closed` is the `None` arm);
the poll form's arms pin identically — `try_recv()`: `Ok(None)` =
idle (no event pending right now), `Err(Closed)` = the receiver
closed, `Database` otherwise (the closed shape annotated
2026-10-11, the close-out pass);
close is terminal and its *cause* is the documented engine
asymmetry — SQLite's receiver closes on watcher death (POC-pinned),
the pg receiver stays open across forwarder reconnects (reconnect
wakes arrive on it) and closes only at engine shutdown. A closed
receiver never reopens; recovery is a fresh `subscribe(consumer)`
resuming from the saved offset.
Consumption is wake-driven with table re-read — the same
mechanism split as queues (durable row, LISTEN/watcher wake,
[ADR-006](decisions/006-wake-and-delivery-contract.md)); the
receiver's engine-side trigger is engine-internal, but events
never require polling to become visible (the queues row's pg
re-poll safety net is an admission of LISTEN loss, not the
posture).
### queues
Durable at-least-once work
([ADR-002](decisions/002-feature-scope.md)). Depth pinned by
[ADR-010](decisions/010-queue-semantics-depth.md); contract-level
obligations:
- `enqueue` / `enqueue_tx` — commit-atomic; `EnqueueOpts { delay,
run_at, priority, max_attempts, expires }`; **field types pinned
(2026-10-07, third review round follow-through)**: `delay`,
`run_at`, `expires` are `Option<i64>` (honker-rs parity,
`packages/honker-rs/src/lib.rs:439-447` — unset is distinguishable
from zero, and the resolution rules below are the unset behavior),
`priority` is a plain `i64` with default **0** (the claim-order
key; the row schema's `DEFAULT 0`), and `max_attempts` is
`Option<i64>` with `None` = the queue-level stamp decides (the
§3a override; no honker counterpart — our field). Every field has
a default-resolved meaning, so all are constructor-settable with
`..Default::default()`. Queue-level
`QueueOpts { visibility_timeout_s, max_attempts, backoff_base_s,
dead_letter_retention_s }` ([ADR-010](decisions/010-queue-semantics-depth.md)
§3/§3a): plain `i64` fields (`dead_letter_retention_s:
Option<i64>`, `None` = forever) with a `Default` providing the
pinned defaults 300/3/5/none (honker parities except retention,
which is ours); `ScheduleOpts { priority, max_attempts, expires }`
types identical to their `EnqueueOpts` twins. **Option semantics
([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md))**:
ready time resolves `delay` → `now + delay` (wins) over `run_at` →
literal absolute over neither → now; `expires` is relative seconds
from enqueue resolved to an absolute row `expires_at` (`None` =
never); the resolved values — not the raw fields — are what
`get_job` shows. The `QueueOpts` stamps land on each job row at
enqueue (§3a), resolved per
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md) §3 by
call shape: the auto-commit `Queue::enqueue` (the one handle-carrying
shape) resolves over that handle's opts; `enqueue_tx` (a
`TxHandle` carries no queue opts) and every other no-handle-open
path — outbox backing queues, scheduler boundary fires — stamp the
engine's derived defaults (plain 300/3/5/none; the outbox's
60/5/5 set).
- `claim_one` / `claim_batch` — exactly-once handout under concurrency
(POC-pinned on both engines); take a caller-supplied `worker_id`
(claimant identity — consumer-local, no reserved rule, non-empty;
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §2); claim
ordering: priority DESC, then
ready-time, then enqueue order (FIFO under equal priority). Claims
return boxed `JobHandle`s — ops + row value (same ADR, §3).
`claim_batch(n)`'s `n` is an extent
([ADR-023](decisions/023-fourth-review-round.md) §2): `n <= 0`
claims nothing (the empty `Vec`, a value — never an error, never
the whole queue).
- Job handle: `ack / retry / fail / heartbeat` on the boxed
`JobHandle` a claim returns ([ADR-019](decisions/019-mechanism-handle-surfaces.md)
§3 — `job(&self) -> &Job` beside the ops; one-shot `self: Box<Self>`
ops like the tx commit). The one-shot/repeatable split is
deliberate: `ack`/`retry`/`fail` consume the handle (each is a
terminal transition of the claim); **`heartbeat(self, extend)`
takes `&self` and is repeatable** — renewal is inherently repeated
within one claim; do not "fix" this asymmetry at implementation
([ADR-019](decisions/019-mechanism-handle-surfaces.md) §3). `ack`
deletes the row;
`heartbeat(extend)` is *renewal* — an absolute reset of the claim
deadline (extend is the new full deadline from now, **not additive**
to elapsed time); late heartbeat refused; **a reclaim consumes an
attempt**; `retry(err, None)` computes the queue's equal-jitter
exponential delay (range pinned by
[ADR-010](decisions/010-queue-semantics-depth.md) §3), `retry(err,
Some(d))` overrides; `fail` = immediate dead-letter. Handle-op
validity (uniform predicate): an op succeeds only while the row is
`processing` and the caller's claim deadline is unexpired — deadline
lapse refuses all handle ops (heartbeats, acks included) and a
reclaim does too; the dual-execution window's worker may still
complete its work but its ack will not land (at-least-once, as
documented); false, not error
([ADR-010](decisions/010-queue-semantics-depth.md) §2). Dead letters:
move-to-dead storage, `get_job` sees dead rows (with
`last_error`/`died_at`), retention via `dead_letter_retention_s`
(default forever), no redrive API
([ADR-010](decisions/010-queue-semantics-depth.md) §1–§4).
`get_job(job_id)` on the handle is an **id-global lookup** — the
handle carries the queue, but the lookup keys on the id alone and
resolves rows in any queue (uniform on all engines; annotated
2026-10-11, the close-out pass — the wave-6 review's recorded
prose ambiguity); the tx-side `get_job_tx(queue, job_id)` takes
the queue name and **is** queue-filtered
([ADR-019](decisions/019-mechanism-handle-surfaces.md) §1,
[ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §1).
`QueueOpts` stamp onto the job row at enqueue (§3a) — queues are
names, not config owners. The remaining v1-skeleton ops (`ack_batch`
— the batch ack applies the uniform predicate per id, lapsed or
non-claimed ids silently not counted; `cancel` — unconditional
delete, not an interrupt) pin in
[ADR-010](decisions/010-queue-semantics-depth.md) §1.
- `sweep_expired()` — moves *every* past-expiry row (any state) to
dead + enforces dead-letter retention — the no-stranded-rows
property ([ADR-010](decisions/010-queue-semantics-depth.md) §5);
the queue-scoped handle form ([ADR-019](decisions/019-mechanism-handle-surfaces.md)
§1 — the name rides the handle); cadence recipe in
[queues.md](queues.md) (no ambient sweeper).
### named locks
TTL-bounded coordination locks. Lock rows live in the same database
as the caller's business data — coordination and data co-locate —
but lock ops are **not** part of the tx seam: there is no `lock_tx`;
a lock held across a business transaction is held by explicit
acquisition before/inside it, not transaction-scoped (acquisition is
a separate auto-commit op; rollback of the business tx does not
release it — the TTL discipline governs).
- `try_lock(name, owner, ttl)` — acquire or fail (`Option<Lock>` —
no-work is a value, not an error); `renew` and `release` on the
lock handle. Release on explicit unlock or TTL expiry.
Re-acquirable after expiry (POC-pinned on Postgres; on SQLite it
rests on the forked substrate's lock machinery — the SQLite-side pin
is in the verification backlog below). `ttl <= 0` is a rejection
(duration kind, [ADR-023](decisions/023-fourth-review-round.md) §2)
— opaque `Err`, never a silent immediately-expired lease.
- **Guarantee row** ([ADR-008](decisions/008-contract-v1-pinning.md)
§7): mutual exclusion bounded by TTL + renewal — after TTL expiry
exclusion lapses *silently* (no revocation event); holders must
`renew` within TTL; expiry is a loss of exclusivity, not an error.
- Lock names are shared-namespace (reserved-prefix rules below,
[ADR-008](decisions/008-contract-v1-pinning.md) §4).
### outbox
A helper over queues, not a separate mechanism: enqueue inside the
business transaction + the delivery/consumption worker entry points.
Same evidence base and guarantee as queues
([ADR-002](decisions/002-feature-scope.md)). Worker semantics
(honker's `run_once` shape, inherited as the pinned posture; the
delivery callable's shape pinned by
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §5):
`run_once(worker_id, delivery) -> bool` is a *pull* op — claim one
job, run
the delivery closure, `ack` on `Ok`, `retry(err, None)` (the queue's
curve) on `Err`; the consumer calls it in its own loop. **No
engine-issued heartbeat inside delivery** (honker parity, inherited
deliberately — the helper does not auto-heartbeat on the caller's
behalf; the boxed `JobHandle`'s own `heartbeat` remains available to
the delivery closure, and renewal inside the deadline is the
consumer's obligation):
delivery slower than the backing queue's stamped visibility timeout
(default 60 s for outbox-backed queues) can have its claim expire
mid-delivery and be redelivered — the dual-execution window;
idempotent delivery is the consumer's obligation, same as queues.
The transactional enqueue side is `outbox_enqueue_tx(outbox, opts,
payload)` on the `TxHandle` trait
([ADR-014](decisions/014-outbox-tx-enqueue.md)): it takes the outbox
name (validated like `store.outbox(name)` — empty → `InvalidName`,
reserved-prefixed → `ReservedName`), derives the backing queue name
engine-side, and lands the job row in the caller's transaction with
`EnqueueOpts` stamped per
[ADR-010](decisions/010-queue-semantics-depth.md) §3a over the backing
queue's derived `QueueOpts`. Commit
makes the job visible to `run_once` exactly when the business write
commits; rollback drops both (the no-ghosts property,
[ADR-007](decisions/007-transactional-seam.md)). The derived backing
queue name is not expressible through `enqueue_tx` (the reserved
prefix is rejected on directly-supplied names) —
`outbox_enqueue_tx` is the only transactional path into it, which is
itself the guarantee that the derivation cannot be collided with.
### scheduler
Collapsed into queues per [ADR-009](decisions/009-scheduler-collapse.md):
`schedule(name, spec, queue, payload, opts)` (upsert by name),
`unschedule(name)`, and the opt-in `run_schedules(stop: StopToken)`
runner (`StopToken` is a core-crate cloneable cancel handle —
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §4; the
leader-elected leadership lock is the reserved name
`__alkstore_scheduler`). Both name-bearing arguments validate at the
entry point: the schedule *name* like a queue name (non-empty,
reserved-prefix rejected —
[ADR-009](decisions/009-scheduler-collapse.md) §1) and, since
[ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §3, the
**queue argument** identically (`InvalidName`/`ReservedName`) — a
schedule cannot target a reserved name, so the outbox backing queue
stays reachable only through the outbox surface
([ADR-014](decisions/014-outbox-tx-enqueue.md)). Schedules never fire
without a runner (no ambient timers). v1 spec grammar:
`@every <n><unit>` only (`s|m|h|d`); cron strings are rejected
(`InvalidSpec`) pending a consumer-inventory row naming wall-clock
cron. Each boundary fire enqueues ordinary work stamped with
`ScheduleOpts { priority, max_attempts, expires }`
([ADR-009](decisions/009-scheduler-collapse.md) §3) — resolved over
the target queue's *derived engine defaults* (visibility/backoff/
retention; no handle is open), per
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md) §3.
Boundary
guarantee: [ADR-009](decisions/009-scheduler-collapse.md) §4
(at-least-once per elapsed boundary, bounded catch-up with
skip-forward past the cap).
## Cross-cutting contracts
### Delivery-guarantee table
The per-mechanism table in
[ADR-006](decisions/006-wake-and-delivery-contract.md) is the contract
of record for notify/streams/queues; the locks row is
[ADR-008](decisions/008-contract-v1-pinning.md) §7's and the scheduler
row is [ADR-009](decisions/009-scheduler-collapse.md) §4's. This spec
inherits them and adds the consumer obligations:
- wake idempotence (notify's best-effort, coalescing behavior);
- explicit offset saves (streams); wake-fire ordering is a
documented tolerance, not a pinned guarantee — wake order need not
match offset order and a wake-driven subscriber can drain past an
event whose publish completed out of offset order (the streams
section's tolerance paragraph, 2026-10-11); recovery rides the
offset-ordered re-read the wake contract already obligates;
- visibility-timeout budgeting — heartbeat inside the deadline for
long work; the dual-execution window and reclaim-eats-attempt rules
are documented consumer obligations
([ADR-010](decisions/010-queue-semantics-depth.md) §2);
- running `run_schedules` for schedules to fire at all, and scheduling
sweep cadences via the collapse recipe — timeliness is the
consumer's, correctness-of-transition the engine's
([ADR-010](decisions/010-queue-semantics-depth.md) §6);
- `*_tx` operations are *only* durable after the caller's commit —
rollback drops job rows, event rows (keyed and unkeyed alike),
notifications, and offset saves together (the no-ghosts property,
POC-pinned on both engines).
### Errors
`thiserror`-typed, per the family standard. Pinned by
[ADR-008](decisions/008-contract-v1-pinning.md) §5: one top-level
`Error`, with v1 variants `PayloadTooLarge` (universal; produced
pg-side, contract-wide matchable), `ReservedName`, `InvalidName`,
`Closed`, `Codec`, and the opaque `Database` fallback (engine detail
preserved via the source chain). `InvalidSpec` (schedule spec grammar)
and `LeadershipLost` (`run_schedules` return on leadership loss) —
both from [ADR-009](decisions/009-scheduler-collapse.md) §6 — are
part of contract v1's initial text (pre-release additions per
[ADR-017](decisions/017-contract-versioning.md) class 1; the
"post-v1 additions" framing an earlier draft used is retired). Pinning rule: a variant exists only when
callers can act differently on it. Queue claim "no work" and job/lock
boolean results are values, not errors; dead-letter is observable
state (`get_job`), not an error.
### Capability surface
None — decided by
[ADR-016](decisions/016-deployment-honesty.md) (2026-10-06): `Store`
carries no capabilities accessor, in v1 and by default ever. The
honest single-host/multi-host boundary lives at compile time (the
engine-crate dependency *is* the deployment statement —
[ADR-001](decisions/001-crate-split.md)'s single-driver binaries) and
in [deployment.md](deployment.md)'s matrix. Runtime carriage of the
one caller-actionable engine asymmetry is the contract-wide
matchable `PayloadTooLarge` variant (Errors above); re-entry requires
a consumer-inventory row naming a runtime-adapt need.
### Naming / reserved namespace
Consumer-visible names (channels, streams, queues, outboxes, locks,
and schedule names) share engine-visible namespaces on Postgres (LISTEN
channel names are server-global per database). The canonical kinds
list is exactly these six (notify/stream/queue/outbox/lock/schedule;
pinned by [ADR-008](decisions/008-contract-v1-pinning.md) §4, outbox
added by [ADR-014](decisions/014-outbox-tx-enqueue.md), schedule names
by [ADR-009](decisions/009-scheduler-collapse.md) §1):
- Reserved prefix **`__alkstore_`**, engine-independent, applies
across all name kinds; the one v1-reserved string is
`__alkstore_listener_reconnected__` (the Postgres reconnect-wake
channel, [ADR-004](decisions/004-postgres-driver.md)).
- Reserved-prefix names are rejected at every entry point — the
name-bearing methods *and their `*_tx` counterparts* — with the
typed `ReservedName` error; empty names with `InvalidName`
([ADR-008](decisions/008-contract-v1-pinning.md) §4 — enforced as
non-empty-after-whitespace-exclusion per that ADR's wave-1 review
annotation: whitespace-only names are `InvalidName` on both name
classes; names are not trimmed). Stream-consumer
names are consumer-local identifiers, not a reserved-namespace kind.
- Engine-derived names in consumer namespaces carry the reserved
prefix (the outbox's backing queue is derived under the prefix —
honker's `_outbox:{name}` scheme is not inherited verbatim).
- SQLite: the substrate's `__alkstore_*` internal table family is
storage-internal (not consumer namespace) — re-owned from upstream's
`_honker_*` by the fork ([ADR-011](decisions/011-sqlite-substrate-fork.md),
designed per [ADR-012](decisions/012-forked-substrate-design.md)).
Postgres: engine tables are
schema-scoped — one engine-owned schema
([ADR-010](decisions/010-queue-semantics-depth.md) §8) — the
*channel* namespace is this section's.
## Verification backlog
Contract properties POC-pinned on one engine only (or sketched rather
than surface-verified) — the contract test suite must pin both engines
before the engine specs are called `stable`:
- **Named-lock TTL/expiry re-acquisition on SQLite** — pinned on
Postgres (pg POC's lock probe); on SQLite it rests on the forked
substrate's lock machinery (its `lock_renew` verified in the quality
read; the re-acquire-does-not-refresh-TTL behavior is upstream's,
inherited deliberately) — pin it in the contract suite.
- **Wake semantics under `WakeReceiver` shapes** — POCs verified
wake delivery/coalescing through their own probe types; the pinned
`Wake { channel }` / recv forms (§3 of
[ADR-008](decisions/008-contract-v1-pinning.md)) need the contract
suite's own property tests on both engines.
- **`save_offset_tx` exactly-once-within-a-business-tx shape** — both
POCs verified it through their *sketch* implementations (SQLite's
offset-save was a plain SQL upsert, not the forked substrate's full
surface;
the pg side likewise through its probe), so the property must be
re-pinned against the real engines' `save_offset_tx` in the
contract suite.
- **Concurrent `try_lock` loser/error behavior** on SQLite (the pg
side returns cleanly; the substrate's busy-path under lock
contention is the thing to pin).
- **Scheduler + depth properties on Postgres** — the SQLite side's
tick/leader/catch-up machinery inherits the forked substrate's
test-pinned implementation; the pg engine's re-derived tick (boundary
advance,
64-cap skip-forward, leadership-loss discipline) and the
[ADR-010](decisions/010-queue-semantics-depth.md) depth properties
(visibility reclaim consuming attempts, dead-letter moves, the
no-stranded-rows sweep) pin in the contract suite at
implementation.
- **Backoff-curve and stamp-resolution equivalence across engines** —
the equal-jitter curve ([ADR-010](decisions/010-queue-semantics-depth.md)
§3) and the opts-stamping resolution (§3a) are computed engine-side
per [ADR-012](decisions/012-forked-substrate-design.md) §2's
contract-blind boundary; the contract suite must pin both engines'
arithmetic to identical outputs.
- **Job-handle validity predicate on both engines** — the uniform
processing-state + unexpired-deadline rule
([ADR-010](decisions/010-queue-semantics-depth.md) §2): the
late-heartbeat boundary (refused exactly when the deadline has
lapsed), the post-lapse-ack-refusal (at-least-once), and the
ack-vs-reclaim race (one wins atomically); `ack_batch` applies the
predicate per id (lapsed or non-claimed ids silently not counted);
`retry`'s refusal is `false` with the row untouched; the
engine-default strings land identically (`fail(None)` →
`"failed"`, exhaustion → `"max attempts exceeded"`); both engines'
SQL pin identical outcomes in the contract suite.
- **`outbox_enqueue_tx` commit-atomicity on both engines**
([ADR-014](decisions/014-outbox-tx-enqueue.md)) — rollback drops
the backing-queue job row together with the business write (no
ghost job); commit makes it claimable by `run_once`; reserved/empty
outbox names rejected identically on the tx path
(`ReservedName`/`InvalidName`); stamped opts visible via `get_job`
equal for both engines.
- **Cross-engine stream equivalence** ([ADR-015](decisions/015-streams-depth.md)
§3/§4) — publish sequence → identical *(per-stream)* offset
sequence → identical
`read_since` output order on both engines, direct and subscriber
reads alike (offsets are global counters engine-side — pg
bigserial, SQLite AUTOINCREMENT — so equivalence is per-stream
relative order, never absolute offset values); keyed/unkeyed
interleavings preserve global FIFO;
`key` round-trips exactly (`None` vs `Some`), `stream`/`created_at`
fields equal.
- **`publish_with_key_tx` commit-atomicity on both engines**
([ADR-015](decisions/015-streams-depth.md) §2) — rollback drops the
keyed event row with the business write (no ghost event); commit
makes it visible to `read_since`/`subscribe`; empty-`Some`-key
`InvalidName` on both engines' tx paths.
- **`PayloadTooLarge` occurrence asymmetry** ([ADR-016](decisions/016-deployment-honesty.md)
§5) — with no capability surface, `PayloadTooLarge` is the one
runtime carriage of an engine asymmetry, so its matchability pins
in the suite: an oversized `notify`/`notify_tx` payload is rejected
client-side by the Postgres engine before any round trip (the
limit in the variant), and the SQLite engine never produces the
variant at any size — engine-agnostic code writes the same match
on both engines and the non-occurring arm simply never fires.
- **`trim_to` semantics on both engines** ([ADR-015](decisions/015-streams-depth.md)
§5) — exact-boundary trim (`<=`), surviving rows keep their offsets
(gaps legal, never renumbered), reads resume at the trim horizon's
first remaining row, saved offsets below the horizon stay valid,
trim wakes nothing, and a concurrent subscriber never loses its
place.
- **Save-offset monotone composition** ([ADR-019](decisions/019-mechanism-handle-surfaces.md)
§6) — direct-form and receiver-form saves interleave without
regression on both engines; a lower-offset save is a silent no-op;
receiver saves land the last-yielded event's offset exactly.
- **Payload encode/decode round-trip on both engines**
([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md) §4) —
same `Value` published/enqueued on both engines stores byte-identical
rows and decodes back to `Value` equality (`Codec` only on
non-serializable target types).
- **Enqueue-opts resolution equivalence** ([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
§1/§2/§3) — delay-over-run_at precedence, relative-expires
resolution, and the scheduler-fired jobs' derived-default stamps
(`get_job`-visible via the row) are identical on both engines;
plain `enqueue_tx` stamps the same plain-queue derived defaults
(300/3/5/none) on both engines — auto-commit-handle enqueues stamp
the handle's opts, no-handle-open shapes stamp derived defaults
(the outbox's 60/5/5 set being the one exception, ADR-014 §1).
- **In-tx read-your-own-writes on both engines**
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §1) —
reads issued on the tx handle see the transaction's own writes
(`get_job_tx` a row the same tx enqueued; `read_since_tx` an event
the same tx published); another engine connection sees none of it
pre-commit; post-rollback the reads' subjects are gone (no-ghosts
for reads).
- **Drop = rollback on both engines**
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §4) —
a handle dropped before commit leaves no job/event/notify/offset
residue (no-ghosts via RAII); afterward the SQLite writer slot is
reusable and the pg client re-poolable.
- **`schedule()` queue-argument validation on both engines**
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §3) —
reserved-prefix and empty queue arguments rejected at registration
with `ReservedName`/`InvalidName`; stored schedules fire only into
non-reserved queue names.
- **Receiver error/close arms on both engines**
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §5) —
`recv()`'s `Err` carries `Database` only; `save_offset`'s `Err`
carries `Database` only; regression saves are silent no-ops;
SQLite's receiver closes on watcher death (terminal `None`); the
pg receiver stays open across forwarder reconnects (reconnect-wakes
arrive; no close) and closes only at engine shutdown.
- **Numeric-domain semantics on both engines**
([ADR-023](decisions/023-fourth-review-round.md) §2) — extents
clamp to empty (`claim_batch(n <= 0)` claims nothing; stream reads
with `limit <= 0` read nothing, never the whole queue/stream), so
the SQLite `LIMIT -1` dialect artifact is dead on both engines;
durations reject (`try_lock`/`renew` with `ttl <= 0` are opaque
`Err`s on both engines); boundaries are total (a negative
`trim_to` horizon deletes nothing, idempotently; the same
totality over non-positive values covers `heartbeat(extend)`'s
relative-deadline argument and `save_offset`'s offset — both
classified 2026-10-11 by the close-out pass, the ADR-023 §2
annotation; their suite legs landed at the wave-8 gate in
`job_handle_validity_predicate` and
`receiver_close_and_save_arms`, uniformly green on all three
engines; the 2026-10-11 general review added the tx-seam form of
the `save_offset` totality — a staged negative seed reads raw
in-tx and stores raw at commit, pinned in
`in_tx_reads_see_own_writes`, again uniformly green on all three
engines).
- **`encode_payload` typed failure**
([ADR-023](decisions/023-fourth-review-round.md) §1) — the helper
returns `Result<Vec<u8>, Error::Codec>`; enqueue/publish store and
decode the exact serialization of the input `Value` (byte-identical
rows cross-engine, the §4 round-trip unchanged), and the failure
arm is typed at the helper — no silent empty-byte fallback exists.
The failure set grew once (the 2026-10-11 general review, applied
pre-publish): a `Value` nested
deeper than the encode-side depth limit (127 containers — a
128th nested open fails to decode, empirically aligned with
serde_json's recursion limit) is a typed `Codec` rejection, not a
serializable-but-undecodable store and not a serializer stack
overflow; the fuzz corpus-replay invariant pins the boundary
(`fuzz/shared/src/payload_codec.rs`).
- **Plain-path SQLite open** ([ADR-023](decisions/023-fourth-review-round.md)
§3) — the SQLite engine's `open` path argument is a plain
filesystem path: a string carrying a `?name=value` suffix is
treated as a filename, no URI interpretation, no connection-
semantics mutation outside the constructor's opts.
- **Watcher cadence default + knob** ([ADR-023](decisions/023-fourth-review-round.md)
§4) — the SQLite store's watcher runs at the inherited 1 ms
default; `SqliteOpts::poll_interval` changes the configured
cadence (config-surface accounting — wall-clock cadence
measurements stay out of the contract suite's deterministic
posture).
## Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| [001](decisions/001-crate-split.md) | Crate split | core + per-engine crates; single-driver binaries |
| [002](decisions/002-feature-scope.md) | Feature scope | inventory-confirmed features only; cut-flags explicit |
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | opaque wake + re-read; notify-vs-streams guarantee split |
| [007](decisions/007-transactional-seam.md) | Tx seam | caller-held handle, `*_tx` methods, native commit-atomicity |
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 pinning | surface partition, `TxHandle` trait shape, `Wake` type, reserved strings, error taxonomy, config split, locks guarantee row |
| [009](decisions/009-scheduler-collapse.md) | Scheduler collapse (pre-release extension) | queues + `schedule()`/`run_schedules`, `@every`-only v1, boundary guarantee row |
| [010](decisions/010-queue-semantics-depth.md) | Queue depth (pre-release extension) | visibility/renewal, opts stamping, backoff curve, dead-letter, sweep, layout |
| [011](decisions/011-sqlite-substrate-fork.md) | Substrate fork | SQLite substrate owned (`__alkstore_*` naming); queue ops re-derived on contract v1 |
| [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate boundary; engine-side formula arithmetic pinned equivalent by the contract suite |
| [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue (amends 008) | `outbox_enqueue_tx` on `TxHandle`; outbox-name validation; derived backing queue reached only through the outbox surface |
| [015](decisions/015-streams-depth.md) | Streams depth (amends 006/008) | key = carried metadata, global-FIFO ordering row, `StreamEvent` shape, `publish_with_key_tx`, `trim_to` |
| [016](decisions/016-deployment-honesty.md) | Deployment honesty (decides 008's parked question) | no runtime capability surface — compile-time engine identity + documented matrix; `PayloadTooLarge` occurrence asymmetry pinned |
| [017](decisions/017-contract-versioning.md) | Contract versioning (governs this surface's changes) | core crate's semver *is* the contract version; four change classes; amend-in-place ends at first release; pairing = manifest pin + version-stamped contract suite + docs |
| [019](decisions/019-mechanism-handle-surfaces.md) | Mechanism handles (amends 008 §1/§2/§8) | handle traits pinned (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`); `worker_id` claimant identity; `Job`/`Schedule` struct shapes; core-owned `StopToken`; save-offset composition |
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options + bridges (amends 008 §1/§2, 009 §3) | `delay` wins over `run_at`; `expires` = relative seconds; scheduler stamps from derived queue defaults; serde_json byte-level payload encoding |
| [021](decisions/021-tx-reads-and-value-shape-fixes.md) | Third review round (amends 008 §2/§8, 019 §1/§3, 009 §1) | tx-side read methods on `TxHandle` (read-your-own-writes); `Job.claimed_at` restored; `schedule()` queue argument validated; drop = rollback; receiver close/error arms pinned |
| [023](decisions/023-fourth-review-round.md) | Fourth review round (amends 020 §4, 008 §5, 012 §4) | numeric-argument domains by kind (extents clamp empty, durations reject, boundaries total); `encode_payload` typed (`Codec`); plain-path SQLite open (URI flag dropped); watcher cadence 1 ms default + `SqliteOpts` knob |
## Open Questions
Open questions are tracked in
[open-questions.md](open-questions.md). Key
questions affecting this document:
- **OQ-10**: contract versioning discipline across engine crates —
**resolved** ([ADR-017](decisions/017-contract-versioning.md)):
the core crate's semver is the contract version; change classes
pinned; the verification backlog below matures into the
version-stamped contract suite (the pairing instrument).
Resolved on this document's surface: **OQ-09** (scheduler collapse —
[ADR-009](decisions/009-scheduler-collapse.md)), **OQ-05** (queue
semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)),
**OQ-13** (transactional outbox enqueue shape —
[ADR-014](decisions/014-outbox-tx-enqueue.md)), **OQ-12** (streams
depth — [ADR-015](decisions/015-streams-depth.md)), **OQ-08**
(capability-surface shape — none, by default ever;
[ADR-016](decisions/016-deployment-honesty.md)), and **OQ-10**
(the versioning discipline this document's surface is governed by —
[ADR-017](decisions/017-contract-versioning.md)), 2026-10-05/06.
The second review round (2026-10-06) found no new open questions —
its findings resolved directly as
[ADR-019](decisions/019-mechanism-handle-surfaces.md) (handle
surfaces, value shapes, stop token) and
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
(enqueue-option semantics, scheduler stamp source, payload encoding).
The third review round (2026-10-07, pre-decomposition) likewise found
no new open questions — its five findings resolved directly as
[ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) (tx-side
reads, `Job.claimed_at`, `schedule()` queue-validation, drop =
rollback, receiver close/error arms).