Third review round follow-through: enqueue_tx stamp source, sweep_expired handle form, with_tx signature, and B-block ambiguity pins

This commit is contained in:
glm-5.3-flash committed 2026-10-07 14:01:36 +00:00
1 parent 08dc1bf011
commit 83767e880b
11 files changed
+258 -101

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-07 (ADR-021 — third review round resolved)
last_updated: 2026-10-07 (ADR-021 + third-round follow-through resolved)
---
# alkstore — Architecture
@@ -27,7 +27,7 @@ pending architecture review and OQ resolution.
| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-10 (resolved) |
| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-06 (resolved), OQ-12 (resolved), OQ-13 (resolved) |
| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-08 (resolved), OQ-12 (resolved), OQ-13 (resolved) |
| [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-06 (resolved) |
| [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-05 (resolved), OQ-09 (resolved), OQ-06 (resolved) |
| [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-08 (resolved) |
| [open-questions.md](open-questions.md) | draft | OQ tracker (promoted from OQ-ST register) | — |
+81 -28
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-07 (ADR-021 — third review round: tx reads, claimed_at, schedule queue validation, drop=rollback, receiver arms)
last_updated: 2026-10-07 (ADR-021 + third-round follow-through: enqueue_tx stamp source, sweep_expired handle form, handle-op/ack_batch/opts-type pins, with_tx signature, payload-limit basis, per-stream offsets)
---
# Core contract
@@ -91,8 +91,9 @@ 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
`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
@@ -141,7 +142,11 @@ 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)).
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
@@ -164,7 +169,10 @@ never sees it.
- `notify(channel, payload)` — payload ≤ 8000 bytes on Postgres
(client-side checked, typed `PayloadTooLarge` before the round
trip, verified by POC #2); no limit on SQLite. The error variant is
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
@@ -289,19 +297,38 @@ Durable at-least-once work
obligations:
- `enqueue` / `enqueue_tx` — commit-atomic; `EnqueueOpts { delay,
run_at, priority, max_attempts, expires }`; **option semantics
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. 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) 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).
`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;
@@ -312,7 +339,13 @@ obligations:
- 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;
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
@@ -332,13 +365,17 @@ obligations:
(default forever), no redrive API
([ADR-010](decisions/010-queue-semantics-depth.md) §1–§4).
`QueueOpts` stamp onto the job row at enqueue (§3a) — queues are
names, not config owners. The remaining v1-skeleton ops (`ack_batch`,
`cancel` — unconditional delete, not an interrupt) pin in
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(queue)` — moves *every* past-expiry row (any state)
to dead + enforces dead-letter retention — the no-stranded-rows
- `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);
cadence recipe in [queues.md](queues.md) (no ambient sweeper).
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
@@ -376,7 +413,11 @@ delivery callable's shape pinned by
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):
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;
@@ -563,8 +604,12 @@ before the engine specs are called `stable`:
([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); both engines' SQL pin
identical outcomes in the contract suite.
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
@@ -573,11 +618,15 @@ before the engine specs are called `stable`:
(`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 offset sequence → identical
`read_since` output order on both engines, direct and subscriber
reads alike; keyed/unkeyed interleavings preserve global FIFO;
`key` round-trips exactly (`None` vs `Some`), `stream`/`created_at`
fields equal.
§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
@@ -609,7 +658,11 @@ before the engine specs are called `stable`:
- **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.
(`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
@@ -56,6 +56,24 @@ handle.commit() / handle.rollback()
wrapper over* the handle shape, not instead of it (the POC verified
the reverse composition doesn't work: a closure cannot outlive
itself, so caching-subscriber state can't escape it).
*(Signature pinned 2026-10-07, third review round follow-through —
the last "shape at implementation" deferral this ADR carried:*
```text
store.with_tx(f) -> Result<()> // on the Store trait
f: FnOnce(&mut dyn TxHandle) -> BoxedFuture<Result<()>>
```
*The trait method is deliberately **non-generic** (a `Store` trait
object must stay object-safe, ADR-008 §2's posture) and returns
`Result<()>`: the closure signals commit/rollback by its own result
(`Ok` ⇒ commit, `Err` ⇒ rollback; drop/panic across the closure ⇒
rollback, ADR-021 §4's disposition) and returns values through the
caller's captured state (the handle never escapes the closure —
the same box-in/box-out composition the POC's closure-scoped arm
(b) verified works, with (a)'s preference unchanged: `with_tx` is
the convenience, `begin_tx` the seam). The closure's future boxes
for object safety. An engine may additionally offer a generic
value-returning `with_tx` convenience on its *concrete* store type;
that ergonomics layer is engine surface, not contract.)*
- Both engines deliver the property **natively** — no emulation:
in-tx notify/Notify delivers only at commit; rollback drops job rows,
business rows, and notifications together (the POC property tests on
@@ -212,7 +212,11 @@ struct Wake { channel: String }
the receiver's forms: `recv() -> None` means closed (source death);
`try_recv`/`recv_timeout` return `Err(Closed)` when the source is
closed, versus `Ok(None)` when merely no wake is pending right now
— closed and idle are distinguishable states.
— closed and idle are distinguishable states. *(Timeout arm pinned
2026-10-07, third review round follow-through: `recv_timeout(d)`'s
expiry without a wake returns `Ok(None)` — idle is the documented
`Ok(None)` arm; `Err` on that method carries `Database` (storage
failure) or `Closed` (source death), never a timeout signal.)*
### 4. Reserved namespace — exact strings
@@ -238,7 +242,14 @@ struct Wake { channel: String }
consumer arguments, `subscribe(consumer)`) are *not* a reserved-
namespace kind — they are consumer-local identifiers, not shared
engine namespaces; no prefix rule applies (an engine may still quote
them).
them). *(Annotated 2026-10-07, third review round follow-through:
the consumer-local identifier classes take the **non-empty rule
only** — an empty stream-consumer name or empty `try_lock` owner is
`InvalidName`, mirroring `worker_id`'s rule
([ADR-019](019-mechanism-handle-surfaces.md) §2) — with no
reserved-prefix rejection (they are not a shared namespace, so a
leading `__alkstore_` is the caller's own local name, no engine
namespace to collide with).)*
- Per-engine internal names:
- SQLite: honker's machinery owns two categories of internal names,
and the contract treats them differently. Its `_honker_*` *table*
@@ -283,7 +294,11 @@ v1 variants (guaranteed-matchable on every engine):
- `PayloadTooLarge { limit }` — **universal variant**, produced by the
Postgres engine's client-side check (8000 bytes, POC #2
verified-before-round-trip) and by contract never on SQLite (no
limit — the documented engine asymmetry). The variant lives in the
limit — the documented engine asymmetry). The measured quantity
*(pinned 2026-10-07, third review round follow-through)* is the
**serde_json serialization of the payload `Value`** — the same
byte string ADR-020 §4 pins as the stored row bytes — the check and
the storage measure one quantity. The variant lives in the
shared taxonomy so the limit asymmetry is visible to engine-agnostic
code — a caller can match it without knowing which engine is behind
the store, and the error itself documents the boundary. The
@@ -397,7 +412,14 @@ trait EventReceiver {
No `save_every`, no save-on-drop — checkpointing is the consumer's
explicit call (the §1 obligation). The consumer's *name* is fixed at
`subscribe(consumer)`; it is a consumer-local identifier (no reserved
prefix, §4).
prefix, §4). *(Annotated 2026-10-07, third review round follow-through:
`EventReceiver` deliberately carries **no `recv_timeout`** — unlike
`WakeReceiver` (§3) — because a stream consumer's steady state is
wake-driven re-read (the queues split applies: the durable row is the
truth, the wake is the hint) and its polling-shaped needs are already
served by `try_recv` and the offset-anchored `read_since`; a
bounded-wait convenience has no row naming it. Stated so the absence
is read as design, not oversight.)*
## Consequences
@@ -66,6 +66,12 @@ Contract job states: **`pending` → `processing` → `dead`** (+ absence:
it. Not an interrupt — the holder's next ack/heartbeat returns
false, the same shape as expiry. `ack_batch` is the batch form of
ack (count returned; non-claimed ids silently not counted).
*(Acknowledged 2026-10-07, third review round follow-through: the
batch form applies §2's full validity predicate — processing state
**and** unexpired claim deadline — per id, not claim-state alone;
an id whose deadline lapsed is silently not counted, identically to
an acked-elsewhere or cancelled id. Per-id outcomes are
independent; partial success is ordinary.)*
- Claim ordering (same queue): `priority DESC`, then ready-time
(`run_at`) ascending, then enqueue order — FIFO under equal
priority, both references agree; pinned.
@@ -224,8 +230,10 @@ budget, exhaustion-by-reclaim, and `expires` lapses (§5).
### 5. `sweep_expired`: the no-stranded-rows property
Contract semantics for `sweep_expired(queue)` (the v1 skeleton's
maintenance entry point):
Contract semantics for `sweep_expired` (the v1 skeleton's
maintenance entry point; the queue-scoped handle form — name on the
handle — is pinned by [ADR-019](019-mechanism-handle-surfaces.md) §1,
2026-10-06):
- It moves to dead (`last_error = 'expired'`) **every row of the
queue past its `expires_at` in any state** — pending rows (honker
@@ -188,9 +188,16 @@ as already pinned):
> offsets immutable and never renumbered.*
This is the row the contract suite pins cross-engine stream
equivalence against: two engines, same publish sequence → same offset
sequence → same `read_since` output order, for direct and subscriber
reads alike. It is deliberately *not* stronger than both engines can
equivalence against: two engines, same publish sequence → same
*(per-stream)* offset sequence → same `read_since` output order, for
direct and subscriber reads alike. *(Per-stream stated 2026-10-07,
third review round follow-through: both engines' offsets are global
counter values — pg bigserial, SQLite AUTOINCREMENT — so a fresh
stream's first event does not carry offset 1; equivalence means the
same publish sequence into one stream produces the same *relative*
offset sequence per that stream, a property safe under either
counter scheme; cross-stream offset values are engine-internal and
never compared.)* It is deliberately *not* stronger than both engines can
implement by a shared SQL shape (`ORDER BY offset ASC` is already
both engines' claim path) and not weaker than the mechanism's purpose
(replay demands monotone positions).
@@ -189,12 +189,23 @@ 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
-> bool // None delay = queue curve
fail(self: Box<Self>, err: Option<String>) -> bool
heartbeat(self, extend: i64) -> bool // absolute reset
}
```
*(Annotated 2026-10-07, third review round follow-through: the
original sketch elided `retry`'s return type — pinned `-> bool`,
the same validity predicate as its siblings. A refused retry —
deadline lapsed, row reclaimed/cancelled/acked elsewhere — leaves
the handle's op **not applied**: the handle was consumed by
`retry(self: Box<Self>, …)` regardless (caller-side), but the row
keeps its pre-call state (still `processing`, or gone if acked/cancelled)
with its original stamps; no dead-letter, no schedule-side delay is
recorded. `false` = the caller's window closed, at-least-once
redelivery governs.)*
— 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
@@ -205,9 +216,18 @@ trait JobHandle {
because renewal is inherently repeated within one claim (the same
one-shot/repeatable split the receiver shapes carry; do not
"fix" the asymmetry at implementation).
`err: Option<String>`: `None` = the engine's
documented-default string (taxonomy-consistent: these strings are
row content, `get_job` data — not matched error variants).
`err: Option<String>`: `None` = the engine's
documented-default string (taxonomy-consistent: these strings are
row content, `get_job` data — not matched error variants).
*(Annotated 2026-10-07, third review round follow-through: the
default string is pinned — **`fail(None)` records `"failed"`**;
`retry(None, …)` that exhausts the budget records
`"max attempts exceeded"`, per
[ADR-010](010-queue-semantics-depth.md) §4's path-based rule —
exhaustion is engine-observed state, so the path's pinned string
wins over the caller-None case; a successful `retry(None, …)`
records nothing (the row returns to pending, `last_error` exists
only on dead rows).)*
*(Struct corrected 2026-10-07 by
[ADR-021](021-tx-reads-and-value-shape-fixes.md) §2: `claimed_at`
was missing from the original pinning —
@@ -105,6 +105,17 @@ parities: 300 s / 3 / 5 s / none), not a handle's opts and not
- **`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.
- *(Annotated 2026-10-07, third review round follow-through: §3a's
"resolved over the queue handle's `QueueOpts`" reading applies only
to the auto-commit `Queue::enqueue` — the one call shape with a
queue handle in scope. Plain
`enqueue_tx(queue_name, opts, payload)` is a no-handle-open shape
too (a `TxHandle` never carries queue opts), so it stamps the
engine's plain-queue defaults (300 s / 3 / 5 s / none) exactly as
boundary fires do; the outbox's `outbox_enqueue_tx` keeps its
60 s/5/5 derived set ([ADR-014](014-outbox-tx-enqueue.md) §1).
Without this note the SQLite and pg implementers could legitimately
diverge on the tx path's stamp source.)*
### 4. The payload encoding: serde_json serialization of the trait `Value`, byte-exactly
@@ -12,6 +12,22 @@ corrections to accepted decisions found by the review — ADR-019 §1's
tx-methods claim and ADR-019 §3's `Job` field list — corrected inline
there, per this repo's convention that wrong statements in decisions
are fixed where they stand, not left for readers to discover.)
*(Follow-through 2026-10-07, same round, second session: the review's
remaining ambiguities — all decidable from decided material — were
closed by single-sentence/annotation edits against the already-decided
ADRs and specs, recorded in the annotations' own text rather than a
new ADR: A4 (`enqueue_tx` stamps the derived engine defaults —
annotated onto ADR-020 §3), A5 (stale `sweep_expired(queue)` shapes
aligned to ADR-019 §1's handle form in core-contract.md and queues.md),
`retry -> bool` + the refused-retry residue and the `fail(None)`
default string (ADR-019 §3), the one-shot/repeatable split restated
(core-contract), `ack_batch`'s per-id predicate (ADR-010 §1), opts
field types/defaults (core-contract, honker-rs parity), the
consumer-local non-empty rules (ADR-008 §4), the `with_tx` signature
(ADR-007), the `PayloadTooLarge` measurement basis (ADR-008 §5),
per-stream offset-sequence equivalence (ADR-015 §4), the
`recv_timeout` timeout arm and `EventReceiver`'s no-`recv_timeout`
posture (ADR-008 §3/§8).)*
## Context
+52 -53
View File
@@ -377,57 +377,6 @@ narrowed to the pinning work its own record already scoped.)*
that removed the substrate's versioning surface), OQ-11 (the
cherry-pick procedure governing the substrate-side non-events).
## Theme: Deployment and capabilities
### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)* — **RESOLVED**
- **Origin**: [deployment.md](deployment.md)
- **Status**: resolved (2026-10-06, Phase 1 —
[ADR-016](decisions/016-deployment-honesty.md))
- **Priority**: medium
- **Resolution**: Pinned by
[ADR-016](decisions/016-deployment-honesty.md): **no runtime
capability surface — in v1 and by default ever**. The honest
single-host/multi-host boundary lives in the two places it is
already true, which compose rather than rival: (1)
**compile-time engine identity** — the engine crate a binary
depends on *is* the deployment statement (single-driver binaries,
[ADR-001](decisions/001-crate-split.md); constructors in engine
crates, [ADR-008](decisions/008-contract-v1-pinning.md) §6 —
"the engine choice is a dependency-graph fact, not a runtime
branch"); (2) **deployment.md's documented matrix** — the
ops-facing facts of record, unchanged. Option 2
(`Store::capabilities()`) rejected field-by-field under ADR-008
§5's act-differently rule generalized to surface: host semantics
admit no in-process action (a flag would invite the engine-type
branch principle 4 bans); payload limits already have their runtime
carriage — the universal, contract-wide matchable
`PayloadTooLarge` variant (a `capabilities()` field would be a
second normative home); wake cadence and knobs are engine-crate
config (§6's split); and no consumer-inventory row names any
runtime-adapt need. No `engine_name()`, no `#[cfg]` capability
axes. The "must not pretend SQLite is multi-host" obligation
resolves into three standing statements (contract text carries
asymmetries via the taxonomy, engine-crate docs carry posture,
deployment.md carries ops facts); the misconfiguration case
(SQLite as shared network storage) follows the family's
deployment-asserts-truth posture (alkblobs precedent) — documented
boundary, no fabricated detection. Contract-suite row added:
`PayloadTooLarge` occurrence asymmetry (pg client-side pre-round-
trip, SQLite never) — with no capabilities API, the variant is the
one runtime carriage of an engine asymmetry, so its matchability
is pinned. Re-entry gate: a consumer-inventory row naming a
runtime-adapt need.
- **Cross-references**: OQ-04 (the pinning that parked this),
OQ-10 (narrowed by this — no capability struct to govern), OQ-11,
[ADR-001](decisions/001-crate-split.md),
[ADR-006](decisions/006-wake-and-delivery-contract.md),
[ADR-008](decisions/008-contract-v1-pinning.md) §1/§5/§6,
[ADR-012](decisions/012-forked-substrate-design.md),
[deployment.md](deployment.md).
## Theme: Core contract (Phase 1 review finds)
### OQ-12: Streams depth — key semantics, `StreamEvent` shape, ordering row, retention — **RESOLVED**
@@ -512,8 +461,9 @@ narrowed to the pinning work its own record already scoped.)*
like `store.outbox(name)` — `InvalidName`/`ReservedName`), derives
the backing queue name `__alkstore_outbox:{outbox}` 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` —
`EnqueueOpts` stamped per
[ADR-010](decisions/010-queue-semantics-depth.md) §3a over the
backing queue's derived `QueueOpts` —
commit-atomic with the business write, rollback drops both (the
no-ghosts property). The reserved-prefix rejection is unchanged: it
governs directly-supplied names; the derivation is legitimate
@@ -533,6 +483,55 @@ narrowed to the pinning work its own record already scoped.)*
(outbox scope row), ADR-007/ADR-008 (the seam design), ADR-010 §3
(the outbox's derived QueueOpts — unchanged by this).
## Theme: Deployment and capabilities
### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)* — **RESOLVED**
- **Origin**: [deployment.md](deployment.md)
- **Status**: resolved (2026-10-06, Phase 1 —
[ADR-016](decisions/016-deployment-honesty.md))
- **Priority**: medium
- **Resolution**: Pinned by
[ADR-016](decisions/016-deployment-honesty.md): **no runtime
capability surface — in v1 and by default ever**. The honest
single-host/multi-host boundary lives in the two places it is
already true, which compose rather than rival: (1)
**compile-time engine identity** — the engine crate a binary
depends on *is* the deployment statement (single-driver binaries,
[ADR-001](decisions/001-crate-split.md); constructors in engine
crates, [ADR-008](decisions/008-contract-v1-pinning.md) §6 —
"the engine choice is a dependency-graph fact, not a runtime
branch"); (2) **deployment.md's documented matrix** — the
ops-facing facts of record, unchanged. Option 2
(`Store::capabilities()`) rejected field-by-field under ADR-008
§5's act-differently rule generalized to surface: host semantics
admit no in-process action (a flag would invite the engine-type
branch principle 4 bans); payload limits already have their runtime
carriage — the universal, contract-wide matchable
`PayloadTooLarge` variant (a `capabilities()` field would be a
second normative home); wake cadence and knobs are engine-crate
config (§6's split); and no consumer-inventory row names any
runtime-adapt need. No `engine_name()`, no `#[cfg]` capability
axes. The "must not pretend SQLite is multi-host" obligation
resolves into three standing statements (contract text carries
asymmetries via the taxonomy, engine-crate docs carry posture,
deployment.md carries ops facts); the misconfiguration case
(SQLite as shared network storage) follows the family's
deployment-asserts-truth posture (alkblobs precedent) — documented
boundary, no fabricated detection. Contract-suite row added:
`PayloadTooLarge` occurrence asymmetry (pg client-side pre-round-
trip, SQLite never) — with no capabilities API, the variant is the
one runtime carriage of an engine asymmetry, so its matchability
is pinned. Re-entry gate: a consumer-inventory row naming a
runtime-adapt need.
- **Cross-references**: OQ-04 (the pinning that parked this),
OQ-10 (narrowed by this — no capability struct to govern), OQ-11,
[ADR-001](decisions/001-crate-split.md),
[ADR-006](decisions/006-wake-and-delivery-contract.md),
[ADR-008](decisions/008-contract-v1-pinning.md) §1/§5/§6,
[ADR-012](decisions/012-forked-substrate-design.md),
[deployment.md](deployment.md).
## Deferred / Blocked
None. **The open question set is empty** — every OQ above is resolved
+8 -5
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-07 (ADR-021 — third review round: tx reads, claimed_at, schedule queue validation)
last_updated: 2026-10-07 (ADR-021 + third-round follow-through: handle-form sweep_expired, ack_batch predicate, opts types)
---
# Queues, scheduler, outbox — semantics depth
@@ -88,9 +88,10 @@ made under; the ADRs carry the WHY.
▼
(gone)
expires lapse (any state) — sweep_expired(q) moves the row to
dead with last_error='expired'; also enforces dead-letter
retention. ack/cancel leave no row; get_job sees dead rows.
expires lapse (any state) — sweep_expired() on the queue handle
moves the row to dead with last_error='expired'; also enforces
dead-letter retention. ack/cancel leave no row; get_job sees
dead rows.
```
- **States**: `pending` → `processing` → `dead` (+ absence:
@@ -151,7 +152,9 @@ made under; the ADRs carry the WHY.
## Sweep / maintenance (ADR-010 §5–§6)
- `sweep_expired(queue)` moves *every* past-`expires_at` row (pending
- `sweep_expired()` on the queue handle (the queue-scoped form,
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §1) moves
*every* past-`expires_at` row (pending
**and** processing) to dead, and enforces dead-letter retention —
the **no-stranded-rows property**
([ADR-010](decisions/010-queue-semantics-depth.md) §5): a job with