ADR-019/020: second review round — handle surfaces, enqueue semantics, payload bridge + mechanical fixes

ADR-019: mechanism-handle traits pinned (Queue/StreamHandle/Outbox/
Lock/JobHandle), Job/Schedule struct shapes, worker_id claimant
identity, core StopToken — closes the 'pin at implementation' residue
before ADR-017's amend-in-place window terminates at first release.

ADR-020: delay-wins-over-run_at precedence (honker parity), relative
expires, scheduler-fired stamps from derived queue defaults, serde_json
byte-level payload encoding.

Mechanical: overview/engine-postgres ADR tables completed through 020,
core-contract Errors section re-framed to class-1, ADR-014 sketch
annotated with publish_with_key_tx, ADR-015 status dated, reserved-
namespace kinds list normalized to six kinds, redrive gate wording
aligned, broken link fixed, StopToken deferral superseded in ADR-009.
This commit is contained in:
glm-5.3-flash committed 2026-10-06 13:06:23 +00:00
1 parent c49befd195
commit 8323a7853e
13 files changed
+661 -54

No files matched your search

+7 -3
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-06 (ADR-018; OQ-11 resolved — question set closed)
last_updated: 2026-10-06 (ADR-019/020 — second review round resolved)
---
# alkstore — Architecture
@@ -53,6 +53,8 @@ pending architecture review and OQ resolution.
| [016](decisions/016-deployment-honesty.md) | Deployment honesty — no runtime capability surface; compile-time engine identity + documented matrix | Accepted |
| [017](decisions/017-contract-versioning.md) | Contract versioning — core crate's semver is the contract version; change classes, pairing carriers, lockstep duties | Accepted |
| [018](decisions/018-provenance-register-and-cherry-picks.md) | Substrate provenance register and cherry-pick procedure — `PROVENANCE.md` in-tree, per-delta category-tagged entries, five-step adoption discipline | Accepted |
| [019](decisions/019-mechanism-handle-surfaces.md) | Mechanism-handle surfaces — handle traits (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`), `Job`/`Schedule` shapes, `worker_id` identity, core `StopToken` | Accepted |
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue-option semantics — delay/run_at precedence, relative `expires`, scheduler stamp source, serde_json payload encoding | Accepted |
## Open Questions
@@ -81,8 +83,10 @@ depth — [ADR-015](decisions/015-streams-depth.md)), ~~OQ-10~~
(fork follow-through — provenance register + cherry-pick procedure,
[ADR-018](decisions/018-provenance-register-and-cherry-picks.md)).
No deferred OQs. With the question set closed, Phase 1 moves to
architecture review closure and the implementation-phase gates.
No deferred OQs. With the question set closed (and the 2026-10-06
second review round resolving its findings directly as ADR-019/ADR-020
rather than as new OQs), Phase 1 moves to architecture review closure
and the implementation-phase gates.
## Document Lifecycle
+111 -36
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-06
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics, payload bridge)
---
# Core contract
@@ -87,13 +87,17 @@ present key must be non-empty → `InvalidName`) and is
point ([ADR-015](decisions/015-streams-depth.md) §2).
Mechanism handles come off the store (or, for transactional variants,
off the handle):
off the handle). **Payload encoding** ([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
§4): the trait crosses `serde_json::Value`; engines serialize it with
serde_json 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.
```text
store.notify(channel, payload) handle.notify_tx(channel, payload)
store.stream(name) -> Stream handle.publish_tx / publish_with_key_tx
store.stream(name) -> StreamHandle handle.publish_tx / publish_with_key_tx
handle.save_offset_tx
(the Stream handle also carries
(the StreamHandle also carries
trim_to — stream-side, outside the
tx seam)
store.queue(name, opts) -> Queue handle.enqueue_tx
@@ -102,20 +106,34 @@ 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) -> Result<()>
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)).
Mechanism handles (`Stream`, `Queue`, `Outbox`, `Lock`) are core-owned
trait objects like the tx handle — engine types never appear in
consumer signatures; their trait methods pin at implementation,
mirroring the `TxHandle` pattern ([ADR-008](decisions/008-contract-v1-pinning.md)
§2's rationale applies identically).
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
@@ -200,11 +218,18 @@ business-tx shape). Depth pinned by
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 saved offset below the trim horizon
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. A saved offset
below the trim horizon
([ADR-015](decisions/015-streams-depth.md) §5) stays a valid
position marker.
- `trim_to(horizon)` — delete events with `offset <= horizon`
([ADR-015](decisions/015-streams-depth.md) §5). The stream-side
- `trim_to(horizon) -> count` — delete events with `offset <= horizon`
([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
@@ -232,14 +257,30 @@ Durable at-least-once work
obligations:
- `enqueue` / `enqueue_tx` — commit-atomic; `EnqueueOpts { delay,
run_at, priority, max_attempts, expires }`; queue-level
run_at, priority, max_attempts, expires }`; **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. Queue-level
`QueueOpts { visibility_timeout_s, max_attempts, backoff_base_s,
dead_letter_retention_s }` ([ADR-010](decisions/010-queue-semantics-depth.md)
§3/§4).
§3/§3a) stamp onto each job row at enqueue, resolved over the
handle's opts (auto-commit/tx paths) or the engine's derived
defaults (outbox backing queues, scheduler boundary fires — the
no-handle-open shapes, ADR-014/ADR-020 §3).
- `claim_one` / `claim_batch` — exactly-once handout under concurrency
(POC-pinned on both engines); claim ordering: priority DESC, then
ready-time, then enqueue order (FIFO under equal priority).
- Job handle: `ack / retry / fail / heartbeat`. `ack` deletes the row;
(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).
- 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). `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
@@ -296,8 +337,11 @@ 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):
`run_once(worker_id, delivery)` is a *pull* op — claim one job, run
(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
heartbeat inside delivery** (honker parity, inherited deliberately):
@@ -312,8 +356,9 @@ payload)` on the `TxHandle` trait
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
`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
@@ -326,15 +371,21 @@ itself the guarantee that the derivation cannot be collided with.
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)` runner
(leader-elected where the engine has peers — the leadership lock is
the reserved name `__alkstore_scheduler`). Schedules never fire
`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`). 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). Boundary
([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).
@@ -372,11 +423,12 @@ inherits them and adds the consumer obligations:
`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). Post-v1 additions:
`InvalidSpec` (schedule spec grammar) and `LeadershipLost`
(`run_schedules` return on leadership loss) — both from
[ADR-009](decisions/009-scheduler-collapse.md) §6, the only
taxonomy deltas so far. Pinning rule: a variant exists only when
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.
@@ -396,11 +448,13 @@ a consumer-inventory row naming a runtime-adapt need.
### Naming / reserved namespace
Consumer-visible names (channels, streams, queues, locks, and
schedule names) share engine-visible namespaces on Postgres (LISTEN
channel names are server-global per database). Pinned by
[ADR-008](decisions/008-contract-v1-pinning.md) §4 (schedule names
added by [ADR-009](decisions/009-scheduler-collapse.md) §1):
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
@@ -503,6 +557,19 @@ before the engine specs are called `stable`:
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.
## Design Decisions
@@ -521,6 +588,8 @@ before the engine specs are called `stable`:
| [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 |
## Open Questions
@@ -544,3 +613,9 @@ depth — [ADR-015](decisions/015-streams-depth.md)), **OQ-08**
[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).
@@ -81,9 +81,11 @@ store.run_schedules(stop) -> Result<()> // runs until `stop`
leadership ends the runner — the consumer's recipe is to respawn it
(documented); the lock loss always precedes any stolen fire, so
respawn never double-fires a boundary.
- **Stop and return semantics**: `stop` is a cancellation token
(a cloneable handle whose flip ends the sleep early — the exact
type shapes at implementation, engine-crate docs). A leadership
- **Stop and return semantics**: `stop` is a cancellation token — a
core-crate cloneable `StopToken` (pinned by
[ADR-019](019-mechanism-handle-surfaces.md) §4, 2026-10-06; the
original "exact type shapes at implementation" deferral is
superseded). A leadership
loss returns `Err(LeadershipLost)` — a distinct, matchable outcome
from the clean `Ok(())` a stop produces; the respawn recipe matches
on it. (One taxonomy note: `LeadershipLost` is a *return-shape*
@@ -54,6 +54,9 @@ One method, added to the `TxHandle` trait:
trait TxHandle {
enqueue_tx(queue, opts, payload) -> job_id
publish_tx(stream, payload) -> offset
publish_with_key_tx(stream, key, payload) -> offset // ADR-015,
// same day — not in this
// ADR's original sketch
notify_tx(channel, payload)
save_offset_tx(stream, consumer, offset)
outbox_enqueue_tx(outbox, opts, payload) -> job_id // the addition
@@ -2,7 +2,8 @@
## Status
Accepted
Accepted (2026-10-05, Phase 1 —
[OQ-12](../open-questions.md)'s resolution)
## Context
@@ -0,0 +1,309 @@
# ADR-019: Mechanism-handle surfaces — handle traits, value shapes, claimant identity, stop token
## Status
Accepted (2026-10-06, Phase 1 — second architecture review round,
pre-decomposition; amends [ADR-008](008-contract-v1-pinning.md) §1/§2/
§8 in place, pre-implementation ([ADR-017](017-contract-versioning.md)
class 1 — no release exists; the amend-in-place frame is still open))
## Context
Contract v1 pinned the `TxHandle`, `WakeReceiver`, and `EventReceiver`
trait shapes exactly, but the mechanism handles were left
deliberately open:
- [core-contract.md](../core-contract.md) (pre-review): "Mechanism
handles (`Stream`, `Queue`, `Outbox`, `Lock`) are core-owned trait
objects … their trait methods pin at implementation, mirroring the
`TxHandle` pattern."
- [ADR-009](009-scheduler-collapse.md) §1 deferred the `run_schedules`
stop parameter's type the same way ("the exact type shapes at
implementation, engine-crate docs").
- `Job` and `Schedule` — both `#[non_exhaustive]` consumer-read types
under [ADR-017](017-contract-versioning.md) §3 — had prose-only
field lists, split across ADR-010 §1/§3a and ADR-009 §1.
- The two `save_offset` forms (the pinned
`save_offset(stream, consumer, offset)` store/tx form and
[ADR-008](008-contract-v1-pinning.md) §8's receiver-side
`save_offset(&mut self)`) had no stated composition.
The second architecture review flagged that "pin at implementation"
no longer holds: [ADR-017](017-contract-versioning.md) makes trait
methods the lockstep-priced additive class and their shapes permanent
contract surface, and that ADR's class-1 window (amend-in-place)
**terminates at the core crate's first release** — and these shapes
ship in that first release. So the pinning must happen in an ADR now,
or the implementation agent would be authoring versioned contract
surface with no decision record behind it.
The decisions below are derived, not invented: honker-rs
(`/workspace/honker` `packages/honker-rs/src/lib.rs` @ f4e53c6) is the
starting artifact every placement so far has mirrored, and its handle
placement is the inherited shape — `[ADR-002](002-feature-scope.md)`'s
"fidelity where kept" posture applied to the *contract* side the way
[ADR-012](012-forked-substrate-design.md) §3 applied it to the
substrate's.
## Decision
### 1. Handle placement mirrors honker-rs — mechanism ops live on the mechanism handles
Naming, construction, and placement (the return types are boxed
trait objects — core-owned traits, engine types never in consumer
signatures, the [ADR-008](008-contract-v1-pinning.md) §2 posture):
```text
store.queue(name, opts) -> Box<dyn Queue>
store.stream(name) -> Box<dyn StreamHandle>
store.outbox(name) -> Box<dyn Outbox>
store.try_lock(name, owner, ttl) -> Option<Box<dyn Lock>>
```
*(Trait-name note: the stream handle's trait is `StreamHandle`, not
`Stream` — `StreamEvent` already occupies the type-name's vocabulary
and a bare `Stream` trait beside `EventReceiver` invites confusion
with tokio's `Stream`. The honker-rs struct name maps to
`StreamHandle`; everything else keeps honker's names.)*
```text
trait Queue {
name(&self) -> &str
enqueue(payload, EnqueueOpts) -> job_id
claim_one(worker_id) -> Option<Box<dyn JobHandle>>
claim_batch(worker_id, n) -> Vec<Box<dyn JobHandle>>
ack_batch(ids) -> count // non-claimed ids not counted
cancel(job_id) -> bool // unconditional delete
get_job(job_id) -> Option<Job> // sees dead rows; a value,
// no ops — see §2
sweep_expired() -> count // this queue's rows
}
trait StreamHandle {
name(&self) -> &str
publish(payload) -> offset
publish_with_key(key, payload) -> offset
read_since(offset, limit) -> Vec<StreamEvent>
read_from_consumer(consumer, limit) -> Vec<StreamEvent>
save_offset(consumer, offset)
get_offset(consumer) -> i64 // absent consumer = 0
trim_to(horizon) -> count // deletes offset <= horizon
subscribe(consumer) -> Box<EventReceiver> // shape: ADR-008 §8
}
trait Outbox {
name(&self) -> &str
enqueue(payload, EnqueueOpts) -> job_id // into the derived
// backing queue
run_once(worker_id, delivery) -> bool // see §4
}
trait Lock {
name(&self) -> &str
renew(ttl) -> bool // new full TTL window from
// now; false = lost it
release(self: Box<Self>) -> bool // consuming; true if held
}
```
- **Why mirror honker's placement**: it is the starting artifact's
proven shape; every prior decision (claim ordering, QueueOpts
stamping, the v1 skeleton's method *names*) already assumed
queue-scoped claim/maintenance ops — `sweep_expired(queue)` on
`Store` would take the name argument that the handle already
carries; a `Queue` handle without `claim_*` would be a construction
ceremony with no behavior. Honker's `Job`-ops-on-claimed-work shape
and `Lock::release(self)` RAII shape are likewise inherited.
- `get_job`, `save_offset`, `get_offset`, `read_*` remain available
from *inside* a transaction through the `TxHandle`'s `*_tx` methods
— the handle forms are the auto-commit convenience counterparts
([ADR-007](007-transactional-seam.md)).
- The **reserved-prefix / name validation rules apply to the
constructors** (`queue`/`stream`/`outbox`/`try_lock`) exactly as
pinned by [ADR-008](008-contract-v1-pinning.md) §4 — nothing new;
the handles carry validated names.
- Trait *method-set* additions on these handle traits post-release
are [ADR-017](017-contract-versioning.md) class 2 — the same
lockstep cost as `TxHandle` additions; stated here so the cost is
priced before any consumer depends on it.
### 2. Claimant identity — `worker_id`, caller-supplied and consumer-local
- `claim_one`/`claim_batch`/`run_once` take a **`worker_id: &str`** —
the claim's ownership token, stamping the row's `worker_id` column.
It is a **consumer-local identifier in the stream-consumer-name
class** ([ADR-008](008-contract-v1-pinning.md) §4: no reserved-
prefix rule, no shared namespace): callers self-supply (a host id, a
task label); the contract requires only non-emptiness
(`InvalidName` otherwise — the v1 skeleton's only name-kind rule
addition, same variant, no new taxonomy).
- Cross-process semantics are the queue mechanism's, not the
identifier's: claim exclusivity comes from the claim statement's
atomicity ([ADR-010](010-queue-semantics-depth.md)), not from id
uniqueness; two workers self-supplying the same string is their
confusion to avoid (ops tooling reads it, nothing enforces it).
### 3. Value shapes — `Job` and `Schedule` pinned as structs
Both `#[non_exhaustive]` per [ADR-017](017-contract-versioning.md) §3
(field additions ride class 2):
```text
struct Job {
id: i64,
queue: String,
state: JobState, // enum: Pending | Processing
// | Dead
payload: Vec<u8>,
priority: i64,
run_at: i64, // the claim-order key
attempts: i64, // every claim counts
max_attempts: i64, // the row's stamp
worker_id: Option<String>, // claimant, None pre-claim
claim_expires_at: Option<i64>, // deadline, None pre-claim
created_at: i64, // unix seconds
expires_at: Option<i64>, // job-level expiry, None =
// never
visibility_timeout_s: i64, // the §3a stamps, dead rows
backoff_base_s: i64, // included (get_job returns
dead_letter_retention_s: Option<i64>, // the stamps with the row)
last_error: Option<String>, // dead-only (None on live)
died_at: Option<i64>, // dead-only
}
```
- Single `Job` type, two carriers: **claims return
`Box<dyn JobHandle>`** — ops + row:
```text
trait JobHandle {
job(&self) -> &Job // the row, as a value
ack(self: Box<Self>) -> bool
retry(self: Box<Self>, err: Option<String>, delay: Option<i64>)
// None delay = queue curve
fail(self: Box<Self>, err: Option<String>) -> bool
heartbeat(self, extend: i64) -> bool // absolute reset
}
```
— and **`get_job` returns `Option<Job>`** (pure data). The *same
uniform validity predicate* ([ADR-010](010-queue-semantics-depth.md)
§2 — `processing` + unexpired caller deadline) governs a
`JobHandle`'s ops; a get_job row yields no handle, so no ghost ops
exist on it. Ops take `self: Box<Self>` mirroring `TxHandle`'s
commit shape (consumed handle, one-shot discipline).
`err: Option<String>`: `None` = the engine's
documented-default string (taxonomy-consistent: these strings are
row content, `get_job` data — not matched error variants).
- `Schedule` (the `schedule()` read-back value):
`Schedule { name: String, spec: String, queue: String, opts:
ScheduleOpts }`.
### 4. `run_schedules(stop)` — the stop token is a core-crate type
```text
store.run_schedules(stop: StopToken) -> Result<()>
#[derive(Clone)] struct StopToken
impl StopToken {
fn cancel(&self)
fn is_cancelled(&self) -> bool
}
```
- **Pinned in core, not deferred**: the parameter type is contract
surface ([ADR-017](017-contract-versioning.md) — a pinned method's
signature), and a core-owned `StopToken` is exactly the
config-split posture ([ADR-008](008-contract-v1-pinning.md) §6)
in miniature — a contract-owned type whose *await mechanics* are
engine-internal (tokio watch per engine; not contract).
- Semantics ([ADR-009](009-scheduler-collapse.md) §1, unchanged):
`cancel()` on any clone flips every clone;
`run_schedules` ends its sleep early and returns `Ok(())` — the
clean-stop return; leadership loss still returns
`Err(LeadershipLost)` regardless of the token.
### 5. `run_once`'s delivery callable
`Outbox::run_once(worker_id, delivery) -> bool` (true = claimed and
processed) — the delivery argument is the consumer-supplied async
callable taking `Box<dyn JobHandle>`, returning the boxed future of
`Result<()>` (object safety: no generic methods on the boxed
handle — the [ADR-008](008-contract-v1-pinning.md) §2 boxed-future
posture applied to the one remaining surface it applies to). Outcome
posture is pinned as before: `Ok` ⇒ ack, `Err(e)` ⇒
`retry(err, None)` (the queue curve). The exact callable *encoding*
(`&mut dyn FnMut…`-form or a named one-method trait object) is
Rust mechanics with one workable answer under object safety — the
semantics above are the contract; the encoding follows §2's pinned
pattern at implementation.
### 6. The two `save_offset` forms compose by monotonicity
One op, two conveniences: `StreamHandle::save_offset(consumer,
offset)` / `save_offset_tx` write the checkpoint; the receiver's
`save_offset(&mut self)` ([ADR-008](008-contract-v1-pinning.md) §8)
writes **the receiver's last-yielded event's offset through the same
op**. Saves are monotone (a save whose offset is below the stored
checkpoint is a silent no-op — inherited: honker's
`ON CONFLICT … WHERE excluded.offset > existing` upsert), so the
forms interleave freely — they cannot regress or fight one another.
No-offset-regression is the pinned discipline: replay is a *read*
concern (`read_since(offset)` works from any offset; saved checkpoints
do not gate reads). Returns `Result<()>` — refusal (regression) is a
no-op, not caller-actionable (the act-differently rule,
[ADR-008](008-contract-v1-pinning.md) §5).
## Consequences
**Positive**
- Every handle a consumer constructs is now fully specified surface:
decomposition off the current text can write the contract-crate
task without inventing contract shapes.
- The last "pin at implementation" deferral in the contract surface
is closed, before the [ADR-017](017-contract-versioning.md) class-1
window that makes such pinning possible ends.
- Placement honesty: the contract mirrors the starting artifact that
every prior queue/lock/stream decision assumed, so no
re-interpretation debt accrues at implementation.
**Negative**
- Four more boxed-handle traits (plus `JobHandle`) for engines to
implement — the same per-trait cost `TxHandle` carries; measured
negligible against commit costs ([ADR-008](008-contract-v1-pinning.md)
§2), but real surface.
- Trait-method additions on five traits now ride ADR-017 class 2
lockstep duties (engine adoption releases are mandatory on trait
additions). Honest pricing, stated before consumers exist.
- `Job`'s field list is pinned wide (all stamps, both namespaces of
timestamps) — growth from here is class-2-minor via
`#[non_exhaustive]`, but the initial text is larger than a minimal
read type would be.
## References
- [ADR-008](008-contract-v1-pinning.md) §1/§2/§4/§5/§8 — the
partition this completes, the boxed-handle/object-safety posture,
the validation rules, the act-differently rule, the
`EventReceiver` shape and rename table this ADR's handle shapes
extend.
- [ADR-007](007-transactional-seam.md) — the auto-commit-vs-`_tx`
counterpart structure the handle forms slot into.
- [ADR-009](009-scheduler-collapse.md) §1 — the
`run_schedules(stop)` method whose stop type §4 pins; the return
semantics §4 restates unchanged.
- [ADR-010](010-queue-semantics-depth.md) §1/§2/§3a — the job
lifecycle, the uniform validity predicate §3's `JobHandle` ops ride,
the stamps `Job`'s fields carry.
- [ADR-017](017-contract-versioning.md) §2 class 1/§3 — the
amend-in-place window this ADR uses (and which its urgency derives
from), and the `#[non_exhaustive]` classifications §3's structs
satisfy.
- honker-rs surface (`/workspace/honker`
`packages/honker-rs/src/lib.rs` @ f4e53c6) — the placement record:
`Queue`/`Outbox`/`Stream`/`Lock` methods, `JobRow`/`Job` split,
`SaveOffset` upsert, `Lock::release(self)`, claim `worker_id`.
- [core-contract.md](../core-contract.md) — the spec this ADR's §1–§5
pin into place.
@@ -0,0 +1,176 @@
# ADR-020: Enqueue option semantics — delay/run_at, expires, scheduler stamp source, payload encoding
## Status
Accepted (2026-10-06, Phase 1 — second architecture review round,
pre-decomposition; pre-implementation depth pinning per
[ADR-017](017-contract-versioning.md) class 1, same frame as
[ADR-014](014-outbox-tx-enqueue.md)/[ADR-015](015-streams-depth.md))
## Context
The second architecture review flagged four semantic holes where an
implementer would otherwise improvise observable behavior:
1. **`delay` vs `run_at` precedence** — [ADR-008](008-contract-v1-pinning.md)
§1 carries both fields in `EnqueueOpts` with only "`run_at` is the
absolute-time counterpart of `delay`" for guidance. Claim ordering
(`priority DESC`, ready-time `run_at` ASC — ADR-010 §1) silently
presupposes an answer: what does an enqueue with both set mean?
2. **`expires` polarity** — the v1 field set has one expiry field;
relative lifetime or absolute deadline? The `expires_at` column and
the sweep predicate depend on the answer.
3. **Scheduler-fired jobs' stamp source** — [ADR-009](009-scheduler-collapse.md)
§3 stamps boundary-fire enqueues "per [ADR-010](010-queue-semantics-depth.md)
§3a", but §3a resolves stamps "over the queue handle's `QueueOpts`" —
and the scheduler's tick enqueues internally, with no queue handle
open. The outbox closed this gap explicitly (derived `QueueOpts`
defaults, ADR-010 §3); the scheduler never did. Two engines could
pick different defaults before the contract suite catches it.
4. **The payload encoding bridge** — [ADR-008](008-contract-v1-pinning.md)
§2 pins payloads crossing the trait as `serde_json::Value`;
[ADR-015](015-streams-depth.md) §3 pins `StreamEvent.payload:
Vec<u8>` ("core value bytes"); `payload_as<T>` (decode side,
error `Codec`) is pinned. The *encode* side — what relationship the
stored bytes bear to the `Value`, without which the round trip and
the cross-engine byte-equality rows are undefined — is stated
nowhere.
All four are decidable now from decided material + the starting
artifact's source; none is a new design space.
## Decision
### 1. `delay` wins over `run_at`; both resolve to the row's ready time
Pinned exactly as honker's enqueue already implements it
(`honker-core/src/honker_ops.rs` `enqueue`: "delay set →
`unixepoch() + delay` (wins over run_at)"):
- `EnqueueOpts { delay, run_at }` resolution precedence:
**`delay` set → ready time = now + delay** (wins);
**else `run_at` set → ready time = run_at literally** (absolute
unix seconds); **else → now** (claimable immediately).
- Both fields stay (honker parity, ADR-008 §1's "carried whole"); the
resolution rule is the contract, and the row stores the resolved
ready time — `get_job` shows the resolved `run_at`, never the
enqueuer's raw fields.
- No validation error for both-set: precedence is defined, not
rejected (an error would be a third behavior for
[ADR-008](008-contract-v1-pinning.md) §5's rule to price, where
honker's defined precedence already exists and suffices).
- `now` is the engine's single second-precision clock (the queues
pin); the same instant is used for the delay resolution and the row
write (one enqueue, one clock read).
### 2. `expires` is relative seconds from enqueue; `None` = never
Pinned as honker implements it (`expires_s: Option<i64>` → row
`expires_at = unixepoch() + s`):
- `EnqueueOpts.expires: Option<i64>` is a **relative TTL in
seconds from the enqueue instant**; the row's `expires_at` is
resolved at enqueue and stored absolutely. `None` = no expiry.
- This makes the stamping story uniform — every enqueue-time field
resolves against the enqueue instant (delay → ready time; expires →
expiry), and `get_job` shows the resolved absolute `expires_at`.
- The consumer who wants absolute-form expiry computes
`run_at`-style arithmetic on its side (`expires = Some(t - now)`);
no second field is added (the v1 field set stays closed — an
`expires_at` absolute twin would be an opts addition, class 3
priced, with no row naming it and the arithmetic available).
### 3. Scheduler-fired jobs' stamps: the target queue's derived defaults, engine-resolved
The boundary-fire enqueue resolves stamps from **the named queue's
derived QueueOpts defaults** — the engine's built-in defaults (honker
parities: 300 s / 3 / 5 s / none), not a handle's opts and not
`ScheduleOpts`:
- The tick enqueues internally; there is no queue handle in scope by
design (queues are names, not registered objects —
[ADR-009](009-scheduler-collapse.md) §5). The stamp source is
therefore the **engine's queue-defaults resolution** — exactly the
"no handle was opened" shape the outbox already resolved (derived
defaults, [ADR-014](014-outbox-tx-enqueue.md) §1; the same posture,
the plain-queue default set instead of the outbox's 60 s/5/5 s
set).
- `ScheduleOpts { priority, max_attempts, expires }` ride *as
themselves* — the two EnqueueOpts-minus-delay/run-at fields
([ADR-009](009-scheduler-collapse.md) §3) apply over the defaults
(`max_attempts` overrides the default 3; `expires` resolves per §2);
the other three stamps (visibility/backoff/retention) take the
engine defaults. One resolution, engine-side, same on both engines
by the contract suite's equivalence pin.
- **`ScheduleOpts` field growth post-release is class 3** (opts
struct, [ADR-017](017-contract-versioning.md) §3's exemption) —
stated so the pricing is visible now.
### 4. The payload encoding: serde_json serialization of the trait `Value`, byte-exactly
- The trait's `payload` parameter is `serde_json::Value`
([ADR-008](008-contract-v1-pinning.md) §2); the engine serializes it
with serde_json and stores **exactly those bytes** — the stored
bytes of a job row or stream event row are, contract-pinned, the
serde_json serialization of the `Value` the publish/enqueue carried.
- `Job`/`StreamEvent`'s `payload: Vec<u8>` fields and `payload_as<T>`
decode those bytes; the error is `Codec` (pinned). Nothing re-encodes
on the way out.
- This makes the **cross-engine byte-equality rows well-defined**: a
`publish` with `Value` V on engine A and engine B stores byte-identical
rows (same serde_json canonical output for the same `Value`), which
is what the stream-equivalence backlog row's byte comparison means.
- Not pinned to a serde_json *feature flavor* (arbitrary-precision
big ints, map key order etc.) beyond serde_json's default shipping
behavior — the suite pins round-trip (publish → read → `payload_as`
→ `Value` equality), not storage-format trivia; engines must use
serde_json (the family-standard JSON codec both POCs used), and a
codec change would itself be a contract change under
[ADR-017](017-contract-versioning.md) class 3 (it changes what
`payload_as` can decode).
## Consequences
**Positive**
- The last four improvisation surfaces close with zero new machinery:
two are honker's existing behavior promoted to contract text, one
reuses the outbox's already-decided derived-defaults posture, one
makes the existing round-trip assumptions explicit.
- `get_job`'s resolved-fields honesty (§1/§2) makes job behavior
inspectable without ambiguity about which form was enqueued.
- The equivalence suite's byte-equality rows get a stated meaning.
**Negative**
- `delay`-wins-over-`run_at` is honker's precedence, carried as-is:
a consumer supplying both gets the delay form's meaning — stated
contract text, but a surprise if unread.
- Relative-only `expires` puts the absolute arithmetic on the
consumer needing it — acceptable at v1 field-set closure (no row
names the second form).
- serde_json's shipping behavior (map order, number handling) is
implicitly in the stored row — pinned to default serde_json, testable
by round-trip, and only visible via `payload_as` anyway.
## References
- [ADR-008](008-contract-v1-pinning.md) §1 (field sets), §2 (the
`Value` payload posture §4 encodes), §5 (§1's no-new-variant
reasoning).
- [ADR-009](009-scheduler-collapse.md) §3/§5 — the fire-enqueue shape
§3 stamps; the names-not-objects posture that makes §3's source the
engine defaults.
- [ADR-010](010-queue-semantics-depth.md) §1 (claim ordering's
`run_at` key), §3a (stamping — §3 completes its source rule), §8.
- [ADR-014](014-outbox-tx-enqueue.md) §1 — the derived-defaults
posture §3 reuses.
- [ADR-015](015-streams-depth.md) §3 — the `Vec<u8>` payload field §4
bridges.
- [ADR-017](017-contract-versioning.md) §2 class 1/§2 class 3/§3 —
the amendment frame; the opts-field pricing §3 states.
- honker-core `enqueue` (`/workspace/honker/honker-core/src/honker_ops.rs`
@ f4e53c6, precedence/expiration doc comment + implementation) — the
§1/§2 behavior's source of record.
- [core-contract.md](../core-contract.md) — the spec this ADR's
§1–§4 pin into place.
+1 -1
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-06
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics, payload bridge)
---
# Deployment
+8 -3
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-06
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics, payload bridge)
---
# Postgres engine
@@ -112,6 +112,9 @@ the record in [ADR-003](decisions/003-sqlite-driver.md).
| [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue | `outbox_enqueue_tx` on `TxHandle`; derivation engine-side inside the caller's tx |
| [015](decisions/015-streams-depth.md) | Streams depth | nullable key column (carried metadata); bigserial offsets, global-FIFO reads; keyed tx publish; `trim_to` as a pool-connection delete |
| [016](decisions/016-deployment-honesty.md) | Deployment honesty | no runtime capability surface — this engine's multi-host posture is stated by its crate identity and docs; `PayloadTooLarge` occurrence pinned contract-suite (this engine produces it, client-side pre-round-trip) |
| [017](decisions/017-contract-versioning.md) | Contract versioning | engine pins core `alkstore = "1.y"` in its manifest; changes touching contract semantics classified under its four-class taxonomy |
| [019](decisions/019-mechanism-handle-surfaces.md) | Handle surfaces | boxed handle traits (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`) implemented over the pooled/claimed rows; `worker_id` stamps the claimant column |
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options | delay-over-`run_at` resolution in the engine's enqueue SQL; scheduler fires stamp from derived defaults; serde_json byte encoding (the equivalence suite's byte rows) |
## Open Questions
@@ -137,6 +140,8 @@ Resolved: **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-12** (streams depth — [ADR-015](decisions/015-streams-depth.md)),
and **OQ-08** (capability surface — none;
[ADR-016](decisions/016-deployment-honesty.md)),
**OQ-08** (capability surface — none;
[ADR-016](decisions/016-deployment-honesty.md)), and **OQ-10**
(contract versioning —
[ADR-017](decisions/017-contract-versioning.md)),
2026-10-05/06.
+3 -1
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-06 (OQ-11 resolved — ADR-018)
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics)
---
# SQLite engine
@@ -133,6 +133,8 @@ family is `__alkstore_*` (ADR-010 §8's naming authorization).
| [016](decisions/016-deployment-honesty.md) | Deployment honesty | no runtime capability surface; single-host posture stated by crate identity + docs; this engine never produces `PayloadTooLarge` (pinned in the contract suite) |
| [017](decisions/017-contract-versioning.md) | Contract versioning | engine pins core `alkstore = "1.y"` in its manifest; substrate cherry-picks are class-4 non-events; adoption duties per its §5 |
| [018](decisions/018-provenance-register-and-cherry-picks.md) | Substrate provenance | `src/substrate/PROVENANCE.md` (identity, delta register — cherry-picks as tagged entries); cherry-picks recorded at adoption, non-versioning |
| [019](decisions/019-mechanism-handle-surfaces.md) | Handle surfaces | boxed handle traits (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`); `worker_id` stamps the row's claimant column; `StopToken` core-owned |
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options | delay-over-`run_at` in the substrate's re-derived enqueue; scheduler fires stamp from derived defaults; serde_json byte encoding |
## Open Questions
+13 -1
View File
@@ -42,6 +42,18 @@ entries;
five-step cherry-pick procedure recorded at adoption). **The Phase 1
question set is closed** — no open OQs remain; Phase 1 moves to
architecture review and the implementation-phase gates.
**Second review round (2026-10-06, pre-decomposition):** the review
found no *new* open questions — every finding was decidable from
decided material, so it resolved directly as
[ADR-019](decisions/019-mechanism-handle-surfaces.md) (handle-trait
surfaces, `Job`/`Schedule` shapes, claimant identity, `StopToken` —
closing the "pin at implementation" residue before ADR-017's class-1
window ends at first release) and
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
(`delay`/`run_at` precedence, relative `expires`, scheduler-fired
jobs' stamp source, the serde_json payload encoding bridge), plus
mechanical fixes (ADR index staleness in overview/engine-postgres,
stale wording, framing drift).
Resolved questions stay listed with their resolution; they are not
deleted.
@@ -137,7 +149,7 @@ narrowed to the pinning work its own record already scoped.)*
surface; the contract is the trait it returns. (7) Locks guarantee
row added to ADR-006's table (TTL-bounded mutual exclusion, silent
expiry); the scheduler row is explicitly transferred to OQ-09's
resolution. A verification backlog (core-contract.md §Verification
resolution. A verification backlog (core-contract.md §Verification
backlog) tracks the one-engine-pinned properties.
- **Cross-references**: OQ-01, OQ-10 (resolved —
[ADR-017](decisions/017-contract-versioning.md), the discipline
+5 -1
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-06
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics, payload bridge)
---
# alkstore — Overview
@@ -80,6 +80,10 @@ Per [ADR-002](decisions/002-feature-scope.md):
| [014](decisions/014-outbox-tx-enqueue.md) | Transactional outbox enqueue (`outbox_enqueue_tx` on `TxHandle`) | Accepted |
| [015](decisions/015-streams-depth.md) | Streams depth (carried-metadata keys, global-FIFO ordering, `StreamEvent`, `trim_to`) | Accepted |
| [016](decisions/016-deployment-honesty.md) | Deployment honesty (no runtime capability surface; compile-time identity + matrix) | Accepted |
| [017](decisions/017-contract-versioning.md) | Contract versioning (core crate's semver is the contract version; change classes, pairing carriers, lockstep duties) | Accepted |
| [018](decisions/018-provenance-register-and-cherry-picks.md) | Substrate provenance register and cherry-pick procedure (`PROVENANCE.md` in-tree, per-delta entries) | Accepted |
| [019](decisions/019-mechanism-handle-surfaces.md) | Mechanism-handle surfaces (handle traits, `Job`/`Schedule` shapes, `worker_id`, `StopToken`) | Accepted |
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue-option semantics (delay/run_at, expires, scheduler stamp source, payload encoding) | Accepted |
## Non-goals
+18 -4
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-05
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics)
---
# Queues, scheduler, outbox — semantics depth
@@ -39,7 +39,11 @@ made under; the ADRs carry the WHY.
- **Job options** (the contract v1 skeleton,
[ADR-008](decisions/008-contract-v1-pinning.md) §1, from the
honker-rs surface): `delay, priority, max_attempts, expires, run_at`
at enqueue; `ack / retry / fail / heartbeat` on the job handle.
at enqueue (`delay`-wins-over-`run_at` resolution and
relative-`expires` pinned by
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md) §1/§2);
`ack / retry / fail / heartbeat` on the boxed `JobHandle` a claim
returns ([ADR-019](decisions/019-mechanism-handle-surfaces.md)).
- **Cut-flag context**: result storage is
[ADR-002](decisions/002-feature-scope.md)'s cut-flag row;
[ADR-010](decisions/010-queue-semantics-depth.md) §7 reconsidered it
@@ -139,7 +143,8 @@ made under; the ADRs carry the WHY.
opt) makes `sweep_expired` enforce a per-queue dead TTL. Honker's
no-retention posture kept as default; the mechanism is ours.
- **No redrive API in v1**: the recipe is `get_job` + fresh `enqueue`.
No consumer names an API shape; re-entry needs an OQ.
No consumer names an API shape; re-entry gates on a
consumer-inventory row first (the ADR-002 row-first discipline).
## Sweep / maintenance (ADR-010 §5–§6)
@@ -187,6 +192,8 @@ Collapsed into queues: no `Scheduler` handle, no schedule objects.
(the no-ambient-timers posture). The runner: leadership lock →
{renew (on loss, return before ticking — `Err(LeadershipLost)`,
distinct from clean `Ok(())` stop) → fire due boundaries → sleep}.
The `stop` parameter is a core-crate cloneable `StopToken`
([ADR-019](decisions/019-mechanism-handle-surfaces.md) §4).
Leadership lock: reserved name `__alkstore_scheduler`
(engine-derived name under the reserved prefix,
[ADR-008](decisions/008-contract-v1-pinning.md) §4).
@@ -201,7 +208,12 @@ Collapsed into queues: no `Scheduler` handle, no schedule objects.
- Each boundary fire enqueues ordinary work: `payload` into the named
queue with `ScheduleOpts { priority, max_attempts, expires }` —
stamped onto the enqueued job per
[ADR-010](decisions/010-queue-semantics-depth.md) §3a; the queue
[ADR-010](decisions/010-queue-semantics-depth.md) §3a; the three
`ScheduleOpts` fields apply over the target queue's **derived engine
defaults** for the stamps the fire doesn't carry (no queue handle is
open — the same derived-defaults posture as the outbox's backing
queue, [ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
§3); the queue
mechanism's guarantees apply, no separate delivery machinery.
- Schedule storage is engine-internal (SQLite: the forked substrate's
`__alkstore_scheduler_tasks` table; Postgres: the engine-owned
@@ -270,6 +282,8 @@ Collapsed into queues: no `Scheduler` handle, no schedule objects.
| [011](decisions/011-sqlite-substrate-fork.md) | Substrate fork | SQLite-side queue ops re-derived in owned code; `__alkstore_*` naming |
| [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate boundary; fidelity posture; port deltas |
| [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue | `outbox_enqueue_tx` on `TxHandle`; derived backing queue reached only through the outbox surface |
| [019](decisions/019-mechanism-handle-surfaces.md) | Handle surfaces | queue ops on the `Queue` handle, `JobHandle` from claims, `worker_id` identity, `Job`/`Schedule` shapes, core `StopToken` |
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options | delay-over-run_at; relative `expires`; scheduler fires stamp from derived queue defaults |
## Open Questions