docs: resolve OQ-04 — contract v1 pinned (ADR-008): surface partition, TxHandle-on-handle-trait, Wake/WakeReceiver, reserved __alkstore_ namespace, error taxonomy, engine-crate constructors, locks guarantee row

This commit is contained in:
glm-5.3-flash committed 2026-10-05 02:23:15 +00:00
1 parent 4391f6e879
commit 7ad8ac56bc
11 files changed
+650 -133

No files matched your search

+11 -8
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-04
last_updated: 2026-10-05
---
# alkstore — Architecture
@@ -24,11 +24,11 @@ pending architecture review and OQ resolution.
| Doc | Status | Purpose | Key OQs |
|---|---|---|---|
| [overview.md](overview.md) | draft | Crate family, feature surface, non-goals, evidence base | — |
| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-04, OQ-08, OQ-09, OQ-10 |
| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-05, OQ-08, OQ-09, OQ-10 |
| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: honker-core/rusqlite mapping | OQ-05, OQ-06, OQ-09 |
| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-05, OQ-08, OQ-09 |
| [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth frame | OQ-05, OQ-09 |
| [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-04, OQ-08 |
| [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-08 |
| [open-questions.md](open-questions.md) | draft | OQ tracker (promoted from OQ-ST register) | — |
## Architecture Decision Records
@@ -42,22 +42,25 @@ pending architecture review and OQ resolution.
| [005](decisions/005-dependency-ownership.md) | Published libraries by default, named fork triggers | Accepted |
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract — opaque wake + re-read; notify-vs-streams split | Accepted |
| [007](decisions/007-transactional-seam.md) | Transactional seam — caller-held `TxHandle`, `*_tx` methods | Accepted |
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 surface pinning — surface partition, `TxHandle` shape, wake type, reserved strings, error taxonomy | Accepted |
## Open Questions
Tracked in [open-questions.md](open-questions.md) (OQ-01..NN; the
Phase 0 register's OQ-ST-01..08 promote one-to-one — OQ-NN mirrors
OQ-ST-NN — with new Phase 1 questions appended after). Highlights,
in suggested resolution order (OQ-04 first):
in suggested resolution order (OQ-04 resolved):
- **OQ-04** (high): contract pinning — exact trait shape, handle
- ~~**OQ-04** (high): contract pinning — exact trait shape, handle
representation, error taxonomy, reserved strings, guarantee rows
for locks/scheduler.
for locks/scheduler~~ — **resolved** (2026-10-05,
[ADR-008](decisions/008-contract-v1-pinning.md); the scheduler
guarantee row transferred to OQ-09).
- **OQ-05** (high): queue semantics depth — retry/backoff/dead-letter/
sweep design.
- **OQ-06** (high): honker-core quality read — fork-trigger gate.
- **OQ-09** (medium): scheduler as first-class mechanism vs queues +
`schedule()`.
`schedule()`; owns the scheduler guarantee row.
- **OQ-06** (high): honker-core quality read — fork-trigger gate.
- **OQ-08** (medium): capability-surface shape.
- **OQ-10** (medium): contract versioning across engine crates.
+146 -56
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-04
last_updated: 2026-10-05
---
# Core contract
@@ -10,8 +10,10 @@ 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. Exact Rust shapes are OQ-04's pinning work;
the *obligations* are this document.
— and pinned against it. The exact 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); the *obligations* are this document.
## Concepts
@@ -22,9 +24,9 @@ the *obligations* are this document.
- **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); lock and scheduler guarantee rows are
OQ-04/OQ-09 resolution items, not assumed.
table (notify/streams/queues), extended with the locks row by
[ADR-008](decisions/008-contract-v1-pinning.md); the scheduler row
rides OQ-09's collapse decision.
*(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
@@ -32,13 +34,28 @@ the *obligations* are this document.
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)).
([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::open(config) -> Store
Store::begin_tx() -> TxHandle
Store::begin_tx() -> Box<dyn TxHandle + Send>
trait TxHandle {
enqueue_tx(name, opts, payload) -> job_id
publish_tx(stream, payload) -> event_id
notify_tx(channel, payload)
save_offset_tx(stream, consumer, offset)
commit(self: Box<Self>) -> Result<()> // or rollback
}
```
Mechanism handles come off the store (or, for transactional variants,
@@ -47,12 +64,23 @@ off the handle):
```text
store.notify(channel, payload) handle.notify_tx(channel, payload)
store.stream(name) -> Stream handle.publish_tx / save_offset_tx
store.queue(name) -> Queue handle.enqueue_tx
store.try_lock(name, ttl) -> Lock
store.queue(name, opts) -> Queue handle.enqueue_tx
store.try_lock(name, owner, ttl) -> Option<Lock>
store.listen(channel) -> Box<dyn WakeReceiver>
store.outbox(name) -> Outbox
```
The exact shape of these handles (traits vs concrete types, `dyn` vs
generic `TxHandle`) is OQ-04's pinning work.
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 contracts
@@ -64,20 +92,24 @@ 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 error, verified by POC #2); no limit on
SQLite. The asymmetry is a documented engine capability, see
OQ-04/OQ-08.
- `listen(channel)` — starts from "now"; delivers
opaque wakes ([ADR-006](decisions/006-wake-and-delivery-contract.md)).
(client-side checked, typed `PayloadTooLarge` before the round
trip, verified by POC #2); 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) — see also
OQ-08.
- `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 at-least-once, possibly coalesced (SQLite) or per-notify
(Postgres), possibly repeated after reconnect. **Consumers must be
idempotent on wake.** (The sketched return type — a wake receiver vs
a subscription handle — is an OQ-04 pinning item, like all exact
shapes here.)
idempotent on wake.**
- Failure surfaces as channel events, never silence: watcher death
closes the receiver (SQLite); the synthetic reconnect-wake (a
reserved channel, [ADR-004](decisions/004-postgres-driver.md)) covers
Postgres connection gaps.
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.
@@ -93,12 +125,17 @@ business-tx shape).
- `publish` / `publish_tx` / `publish_with_key` — append to the
stream's durable log.
- `read_since` / `read_from_consumer(offset)` — cursor-based reads.
- `read_since` / `read_from_consumer(offset)` — cursor-based reads;
`get_offset(consumer)` — checkpoint inspection.
- `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).
- `subscribe(consumer)` — durable consumption: attach, read to
current tail, resume after restart from the stored offset.
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).
- `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).
### queues
@@ -108,9 +145,12 @@ is [queues.md](queues.md)'s (OQ-05); the contract-level obligations
here:
- `enqueue` / `enqueue_tx` — commit-atomic; `EnqueueOpts { delay,
priority, max_attempts, expires, ... }`.
- `claim` / `claim_batch` — exactly-once handout under concurrency
(POC-pinned on both engines).
run_at, priority, max_attempts, expires }` (the v1 skeleton,
[ADR-008](decisions/008-contract-v1-pinning.md) §1; deeper
`QueueOpts`/retry surface rides OQ-05).
- `claim_one` / `claim_batch` — exactly-once handout under concurrency
(POC-pinned on both engines); `cancel`, `get_job` — job management
and inspection.
- Job handle: `ack / retry / fail / heartbeat`; visibility timeouts
make at-least-once *safe* (work re-appears if a claimant dies).
- `sweep_expired` — maintenance entry point (design in
@@ -120,12 +160,18 @@ here:
TTL-bounded coordination locks, transactional-friendly:
- `try_lock(name, ttl)` — acquire or fail; `renew`; release on
explicit unlock or TTL expiry. Re-acquirable after expiry
(POC-pinned on Postgres; on SQLite it rests on honker's machinery —
the SQLite-side pin is in OQ-04's verification backlog).
- Lock names are shared-namespace (contract text pins collision
rules with the reserved namespace, OQ-04).
- `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 honker's machinery — the SQLite-side pin is in the
verification backlog below).
- **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
@@ -159,26 +205,69 @@ the consumer obligations:
### Errors
`thiserror`-typed, per the family standard. The taxonomy (what
callers match on per mechanism; engine-capability errors like
`PayloadTooLarge` on pg vs no-limit on SQLite) is OQ-04's pinning
work.
`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). 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.
### Capability surface
Whether the `Store` exposes engine capabilities at all — and if so,
which (payload limits, host semantics, wake-cadence knobs) — is
OQ-08's decision ([deployment.md](deployment.md)).
None in contract v1. Whether the `Store` exposes engine capabilities
at all — and if so, which (payload limits, host semantics,
wake-cadence knobs) — is OQ-08's decision
([deployment.md](deployment.md)).
### Naming / reserved namespace
Consumer-visible names (channels, streams, queues, locks) share
engine-visible namespaces on Postgres (LISTEN channel names are
server-global per database). Exact rules: OQ-04. Decided already: a
**reserved/meta namespace exists** for engine-internal names — the
Postgres reconnect-wake channel, any internal bookkeeping — and
consumer names must not collide with it
([ADR-006](decisions/006-wake-and-delivery-contract.md)).
server-global per database). Pinned by
[ADR-008](decisions/008-contract-v1-pinning.md) §4:
- 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). 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: honker's `_honker_*` internal table family is storage-
internal (not consumer namespace); Postgres: queue/stream tables are
schema-scoped (layout rides OQ-05) — 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`:
- **Lock TTL/expiry re-acquisition on SQLite** — pinned on Postgres
(pg POC's lock probe); on SQLite it rests on honker's machinery
unverified.
- **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 full honker 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; honker's busy-path under lock contention is
the thing to pin).
## Design Decisions
@@ -188,6 +277,7 @@ consumer names must not collide with it
| [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 |
## Open Questions
@@ -195,10 +285,10 @@ Open questions are tracked in
[open-questions.md](open-questions.md). Key
questions affecting this document:
- **OQ-04**: exact trait-surface pinning — handle shape, error
taxonomy, reserved strings, rename/grouping decisions (open,
high priority)
- **OQ-10**: contract versioning discipline across engine crates (open)
- **OQ-08**: capability-surface shape (open)
- **OQ-09**: scheduler as first-class mechanism vs queues + schedule
(open)
- **OQ-09**: scheduler as first-class mechanism vs queues + schedule —
also owns the scheduler guarantee row ([open](open-questions.md))
- **OQ-10**: contract versioning discipline across engine crates
([open](open-questions.md))
- **OQ-08**: capability-surface shape ([open](open-questions.md))
- **OQ-05**: queue semantics depth — the v1 skeleton's extension path
([open](open-questions.md))
@@ -2,8 +2,9 @@
## Status
Accepted (the contract skeleton the evidence supports); remaining
surface pinning is OQ-04's contract work against this ADR
Accepted (the contract skeleton the evidence supports); the surface
pinning is done — [ADR-008](008-contract-v1-pinning.md) §3/§4 pin the
wake type and the reserved strings
## Context
@@ -74,9 +75,9 @@ coalesced-but-complete re-read on SQLite).
The Postgres forwarder's synthetic reconnect-wake needs a reserved
channel namespace no consumer channel may collide with. The naming
convention for reserved/meta channels (and any internal queue/stream
names) is pinned in the core contract — exact strings are OQ-04
contract work; the *existence of a reserved namespace* is decided
here.
names) is pinned in the core contract — exact strings pinned by
[ADR-008](008-contract-v1-pinning.md) §4; the *existence of a reserved
namespace* is decided here.
### The caching-subscriber pattern rides the same contract
@@ -116,8 +117,8 @@ consumer-inventory-row-gated extension — not assumed now.
§"What feeds where"; `docs/research/poc-pg-posture-findings.md`
§Sub-module W.
- OQ-ST-04 (`docs/research/phase-0.md`) — de-risked by these
measurements; the contract remainder is OQ-04 +
`core-contract.md`.
measurements; the contract remainder resolved by
[ADR-008](008-contract-v1-pinning.md) over `core-contract.md`.
- [ADR-002](002-feature-scope.md) — notify/listen and streams are
first-class; the guarantee split is why both exist.
- [ADR-004](004-postgres-driver.md) — the forwarder owning the
@@ -3,7 +3,8 @@
## Status
Accepted (the seam shape both POCs verified); handle-type ergonomics
(generic vs downcast) is OQ-04 contract work
decided by [ADR-008](008-contract-v1-pinning.md) §2 — the `*_tx`
methods live on the handle trait, no downcast
## Context
@@ -58,7 +59,8 @@ contract-pinning input rather than open risk:
1. The `as_any_mut` downcast per engine for `*_tx` methods on a
`dyn TxHandle` — small, but a generic or enum-favored handle may be
cleaner. OQ-04 decides with the full contract.
cleaner. Decided by [ADR-008](008-contract-v1-pinning.md) §2
(with the full contract).
2. Thread-affinity of rusqlite tx ops (SQLite handle ops must
round-trip the same blocking thread) — inherent to [ADR-003]'s
bridge, documented as a contract note ("SQLite tx ops are
@@ -0,0 +1,410 @@
# ADR-008: Contract v1 surface pinning
## Status
Accepted (2026-10-05, Phase 1 — OQ-04's resolution; binds
[core-contract.md](../core-contract.md); engine specs map it)
## Context
OQ-04 (promoted from OQ-ST-04) scoped the contract work: with both
engines POC-verified ([ADR-003](003-sqlite-driver.md),
[ADR-004](004-postgres-driver.md)) and the load-bearing decisions made
([ADR-006](006-wake-and-delivery-contract.md) wake contract + guarantee
table, [ADR-007](007-transactional-seam.md) caller-held tx seam), what
remained was pinning the exact trait surface against the honker-rs
starting artifact (`/workspace/honker`
`packages/honker-rs/src/lib.rs` v0.5.0, the §Interface finding in
phase-0.md). The unpinned items:
- Which surface parts are contract v1 vs engine-extension.
- `TxHandle` representation — the POC sketch used `dyn` + per-engine
`as_any_mut` downcast (two recorded frictions,
[ADR-007](007-transactional-seam.md)).
- The reserved namespace's exact strings (POC used
`__listener_reconnected__` ad hoc).
- The error taxonomy (e.g. is `PayloadTooLarge` universal when SQLite
has no 8000-byte limit?).
- What `listen()` returns and where the honker-rs surface gets renamed.
- The `Store::open` config split (contract vs engine-crate option).
- Delivery-guarantee rows for locks and scheduler (ADR-006's table
covered notify/streams/queues only).
- A verification backlog for properties POC-pinned on one engine only.
All of this is paper over a complete evidence base: the honker-rs
surface listing, both POC findings (including the trait sketches and
their recorded frictions), the consumer inventory rows
([ADR-002](002-feature-scope.md)), and the guiding principles
(phase-0.md — engine-agnostic consumer API, no engine-type branching).
## Decision
### 1. Contract v1 partition
**Contract v1** (the core crate's trait surface, both engines must
implement identically):
- notify / listen — `notify(channel, payload)`; `listen(channel)`.
- streams — `publish`, `publish_with_key`, `read_since`,
`read_from_consumer`, `save_offset`, `get_offset`, `subscribe`
(durable log, explicit offsets — the auto-checkpoint ambiguity is
not inherited, per [ADR-006](006-wake-and-delivery-contract.md)).
- queues v1 skeleton — `enqueue`, `claim_one`, `claim_batch`,
`ack_batch`, `cancel`, `get_job`, `sweep_expired`; job handle
`ack / retry / fail / heartbeat`; `EnqueueOpts { delay, run_at,
priority, max_attempts, expires }` — the honker-rs field set,
carried whole (the POC sketch's shape; `run_at` is the absolute-time
counterpart of `delay`, not dropped).
- locks — `try_lock(name, owner, ttl) -> Option<Lock>` with `renew`
and `release`.
- outbox helper — `outbox(name)` with `enqueue` + `run_once` delivery
worker.
- the tx seam — `begin_tx` / commit / rollback and the `*_tx` methods
([ADR-007](007-transactional-seam.md)).
- `subscribe(consumer) -> Box<dyn EventReceiver>` — the durable
stream subscription handle, delivering `StreamEvent`s with an
explicit `save_offset` on the receiver (no auto-checkpoint;
§8 defines the shape).
**Not in v1 — deliberately owned elsewhere:**
- **Queue semantics depth** (retry/backoff curve, dead-letter
move-vs-flag, visibility-renewal mechanics, sweep cadence, `QueueOpts`
fields beyond the v1 skeleton) — OQ-05's design surface. v1 pins the
skeleton above; depth additions extend the contract later.
- **Scheduler surface shape** (first-class mechanism vs queues +
`schedule()`) — OQ-09 decides; the scheduler surface (if any) is not
part of contract v1.
- **Capability flags** — OQ-08 decides whether `Store` exposes them at
all; v1 has no capability surface.
- **`claim_waker`** — dropped from the contract surface; wake-driven
claim is the engine's consumption posture
([engine-postgres.md](../engine-postgres.md)'s LISTEN-driven claim),
engine-internal.
- **Rate limits, result storage** — cut-flag rows
([ADR-002](002-feature-scope.md)); re-enter only via a
consumer-inventory row.
- **Notification payloads on wakes** — wake is opaque
([ADR-006](006-wake-and-delivery-contract.md)); the payload carrying
honker's `Notification` type happens to provide is *not* surfaced in
the contract (see §3).
### 2. `TxHandle` representation: the `*_tx` methods live on the handle trait
The downcast friction dissolves by moving the ops, not by choosing a
handle encoding. The POC sketches put `enqueue_tx` etc. on the
engine/store trait taking `&mut dyn TxHandle`, forcing each impl to
downcast the handle to its concrete type. The contract instead defines
a **`TxHandle` trait carrying the `*_tx` methods directly**:
```text
trait TxHandle {
enqueue_tx(name, opts, payload) -> job_id
publish_tx(stream, payload) -> event_id
notify_tx(channel, payload)
save_offset_tx(stream, consumer, offset)
commit(self: Box<Self>) -> Result<()> // or rollback
}
```
*(Sketches elide `Result<>` wrappers on `*_tx` returns for brevity —
every fallible op returns the taxonomy of §5; `notify_tx` can fail
with `PayloadTooLarge`/`ReservedName` exactly like its auto-commit
counterpart.)*
- `Store::begin_tx() -> Box<dyn TxHandle + Send>` — callers hold the
boxed handle across await points.
- Each engine implements `TxHandle` for its own concrete handle —
**no downcast, no enum, no generic parameters**. The friction the
POCs recorded disappears because there is no engine-store dispatch
of a handle argument; the handle *is* the object.
- Object safety constraints that follow: no generic methods on
`TxHandle`, payload serialization is value-typed in core
(`serde_json::Value` — the type both POCs used; not generic over
`T: Serialize`); `commit`/`rollback` take `self: Box<Self>`
(dispatchable on a boxed trait object).
- **Why not the enum** (one variant per engine): the enum's payload
types would make core depend on engine types — inverting ADR-001's
dependency direction (engines depend on core, never the reverse) —
or make the variants engine-opaque, which re-introduces downcast one
step further away. A new engine (a stated ADR-001 goal) must not be
a core-crate enum edit.
- The generic-vs-dyn alternative buys static dispatch for a hot-path
cost the POCs measured as negligible against the seam itself (SQLite
hop 0.35 ms p50, pg 2.4 ms p50 — both dominated by commit costs);
it is the recorded optimization if ever needed (same posture as
ADR-003's dedicated-bridge optimization).
- Async dispatch over a boxed handle uses the desugared boxed-future
form (the family-standard async trait posture); its per-op cost is
bounded by the same measurements.
- Thread-affinity note retained verbatim: SQLite tx ops serialize
through the writer slot ([ADR-007](007-transactional-seam.md)) — a
documented engine note, not a contract item.
### 3. Wake type and the `listen()` return
`listen(channel)` returns a **wake receiver**, not honker's
subscription:
```text
listen(channel) -> Box<dyn WakeReceiver>
trait WakeReceiver {
recv() -> Option<Wake> // None = source closed (watcher death)
try_recv() -> Result<Option<Wake>>
recv_timeout(d) -> Result<Option<Wake>>
}
struct Wake { channel: String }
```
- **`Wake` carries only the channel name** — the one piece of semantic
content [ADR-006](006-wake-and-delivery-contract.md) allows. Honker's
`Notification { id, channel, payload }` is not surfaced: the pg
engine would have to discard payloads to honor the opaque contract,
and honoring the contract uniformly is what makes consumer code
engine-agnostic. Consumers needing content use streams (the
guarantee split, ADR-006). The SQLite engine's payload transport is
a non-contract implementation detail.
- Receiver close is the failure surface: SQLite watcher death closes
the receiver (`recv() -> None`,
[ADR-006](006-wake-and-delivery-contract.md)); the Postgres
reconnect-wake arrives as `Wake { channel:
Reserved::LISTENER_RECONNECTED }` on every subscriber's receiver
(a broadcast-to-all-subscribers fanout — engine behavior recorded in
[engine-postgres.md](../engine-postgres.md)).
- `WakeReceiver` methods have no wake-count or id: wakes are hints,
their multiplicity is not part of the contract (coalescing on
SQLite, per-notify on Postgres — ADR-006). Close semantics across
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.
### 4. Reserved namespace — exact strings
- **Reserved prefix: `__alkstore_`** (leading double underscore). The
namespace exists per [ADR-006](006-wake-and-delivery-contract.md);
this pins its string. Engine-independent — applies to channel,
stream, queue, and lock names on every engine.
- **The one v1-reserved string:**
`__alkstore_listener_reconnected__` — the Postgres forwarder's
synthetic reconnect-wake channel ([ADR-004](004-postgres-driver.md)).
The POC's ad-hoc `__listener_reconnected__` string is renamed into
the namespace at implementation; nothing (consumer or contract text)
depended on the ad-hoc string.
- Consumer names with the reserved prefix are **rejected at the entry
points** — the name-bearing surface methods (`notify`, `listen`,
`stream`, `queue`, `outbox`, `try_lock`) *and their `*_tx`
counterparts* (`notify_tx` at minimum; any `*_tx` op taking a name) —
with the typed `ReservedName` error (taxonomy, §5). Engine-side
validation obligation: reject before any engine round trip, on both
the auto-commit and tx paths (validation must not be bypassable by
the commit-atomic path). Stream-consumer names (`save_offset(_tx)`
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).
- Per-engine internal names:
- SQLite: honker's machinery owns two categories of internal names,
and the contract treats them differently. Its `_honker_*` *table*
family (`_honker_dead`, `_honker_locks`, …) is storage-internal —
not part of any consumer namespace; its fate rides the quality
read (OQ-06, [ADR-005](005-dependency-ownership.md)). Its
**consumer-namespace derived names** are a real collision surface:
honker-rs materializes an outbox's backing queue as
`_outbox:{name}` inside the queue-name namespace — a consumer
calling `queue("_outbox:foo")` directly could collide with
`outbox("foo")`. The contract's rule: **engine-derived names in
consumer namespaces carry the reserved prefix** — the SQLite
engine's outbox backing queue is `__alkstore_outbox:{name}`,
derived from the consumer's outbox name, and every entry point
rejects the reserved prefix for directly-supplied names (so a
consumer can never create or collide with it). Honker's
`_outbox:` scheme is not inherited verbatim.
- Postgres: queue/stream tables are schema-scoped to avoid
colliding with consumer tables (the co-tenancy precedent; layout
decision rides OQ-05's namespace bullet,
[queues.md](../queues.md)). The reserved *channel* namespace is
this section's; the outbox backing queue obeys the same
reserved-prefix rule as SQLite's.
- Channel-name charset: the contract requires non-empty names and the
reserved-prefix rejection; identifier quoting for Postgres LISTEN /
`pg_notify` is the engine's obligation (POC #2 owned the quoting
pitfall — test-pinned).
### 5. Error taxonomy
One top-level `Error` in the core crate, `thiserror`-typed (family
standard). **Pinning rule: an error gets its own matchable variant
only when the caller can act differently on it** — engine-specific
conditions that a caller cannot act on stay inside the opaque
fallback with their detail in the source chain. This is the line that
keeps the taxonomy small and keeps it from inheriting honker's
per-binding sprawl.
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
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
large-payload alternative (a table row with the id in the
notification — the outbox shape) is the documented workaround.
- `ReservedName { name }` — rejected reserved-prefix names (§4).
- `InvalidName { name }` — empty names and other entry-point
validation failures (§4).
- `Closed` — an operation against a closed receiver/source (also the
`try_recv` / `recv_timeout` form of the wake close in §3).
- `Codec` — payload serialization/deserialization failures
(`payload_as` on stream events and job payloads; wakes carry no
payload per §3).
- `Database` — everything else: driver/connection/SQL errors, opaque
to the contract, engine detail preserved via the error source chain.
No engine-specific variants are minted for its contents.
Explicitly *not* taxonomy items (callers act via value results, not
errors): queue claim returning none (no work); job-handle ops
(`ack`/`retry`/`fail`/`heartbeat`) returning "did the op land" booleans
(`false` = the job was no longer claimable/owned — expired, acked
elsewhere, cancelled); `try_lock` returning `Option<Lock>`; lock
`release`'s boolean.
### 6. `Store::open` config shape — engine options, not contract surface
- The core crate defines the `Store` trait (the §1 surface plus
`begin_tx`). **Constructors and their option structs live in the
engine crates** (`alkstore_sqlite::open(path, SqliteOpts)` /
`alkstore_postgres::open(url, PgOpts)`): durability knobs, pool
sizing, pragmas, watcher cadence, listener posture are
engine-configuration concerns —
[deployment.md](../deployment.md) carries the knob
facts, engine-crate docs carry the options.
- What is contract: the trait the constructor returns, and the
constructor's own obligation — a `Store` handed out by an engine
crate honors the full v1 surface.
- No capability surface in v1 (OQ-08 owns whether one ever exists).
- Consumer code stays engine-agnostic by depending on core and being
constructed by exactly one engine crate (ADR-001's single-driver
binaries) — the engine choice is a dependency-graph fact, not a
runtime branch.
### 7. Delivery-guarantee rows — locks pinned, scheduler owned by OQ-09
[ADR-006](006-wake-and-delivery-contract.md)'s table is extended by
this decision:
| Mechanism | Durability | Replay | Atomicity | Guarantee |
|---|---|---|---|---|
| **locks** | row-backed (machinery-defined) | n/a | acquire/release atomic | **mutual exclusion bounded by TTL + renewal discipline**: a lock excludes other holders while held (explicit release or within TTL); after TTL expiry exclusion lapses *silently* — no revocation event, exclusivity resumes with re-acquisition; a holder renewing inside its TTL keeps exclusion |
- The consumer obligation this row carries: long held-lock work must
`renew` within TTL; expiry is not an error but a *loss of exclusivity*
(the honest TTL semantics the alkblobs fleet-sweeper ADR already
treats as ground).
- The **scheduler guarantee row is not pinned here** — it is defined
by OQ-09's collapse decision (under collapse, the scheduler inherits
the queues row; the scheduler-tick/leader-election guarantees become
part of OQ-09's and OQ-05's resolution text). This is an explicit
transfer to OQ-09's scope, not an open residue of this ADR.
### 8. Renames and grouping deltas from the honker-rs surface
Pinned deltas (the starting artifact's names, where the contract
differs):
| honker-rs | contract v1 | Reason |
|---|---|---|
| `Subscription` (listen) | `WakeReceiver` / `Wake` | wake is opaque — no payload, no id; name must not suggest subscription semantics (no replay, ADR-006) |
| `Notification { id, channel, payload }` | `Wake { channel }` | §3; payload is non-contract |
| `Lock::heartbeat(ttl)` | `Lock::renew(ttl)` | renew describes the TTL discipline the guarantee row pins; heartbeat is the queue-side renewal term |
| `Queue::claim_waker()` | (not surfaced) | engine-internal consumption wake (§1) |
| `StreamSubscription` (`save_every`, auto-save on drop) | `subscribe(consumer) -> Box<dyn EventReceiver>`, explicit saves only | the explicit-offset obligation (core-contract.md streams; the auto-checkpoint ambiguity is not inherited, [ADR-006](006-wake-and-delivery-contract.md)) |
| `update_events()` | (not surfaced) | superseded by the wake subscription — the pinned `WakeReceiver` *is* the reactive-event surface; honker's separate raw update-event stream has no contract role |
| `prune_notifications` / `prune_notifications_keep_latest` | (not surfaced) | notifications-table maintenance tooling on the SQLite engine; disposition rides OQ-05's sweep/maintenance design (consumer-visible only if that design surfaces it) |
| `Scheduler` surface | (out of v1, OQ-09 decides) | §1 |
| `try_rate_limit` / `save_result` / `get_result` / `sweep_results` | (cut-flag rows) | [ADR-002](002-feature-scope.md) |
Grouping stays honker-shaped otherwise: mechanism handles off the
store (`stream(name)`, `queue(name, opts)`, `outbox(name)`), the tx
handle off `begin_tx`, payloads serialized through core value types
(non-generic trait methods, §2), `payload_as<T>` as the decode
convenience on concrete returned values (`Job`, `StreamEvent`) — the
deserialization error is `Codec`, not a per-type variant.
The stream subscription handle's shape (defined here, since §1 pins
the return type):
```text
trait EventReceiver {
recv() -> Option<Result<StreamEvent>> // None = stream closed
try_recv() -> Result<Option<StreamEvent>>
read_since(offset, limit) -> Result<Vec<StreamEvent>>
save_offset(&mut self) -> Result<()> // explicit only — the
// offset advances per read;
// the save is the consumer's
// checkpoint call
offset() -> i64 // the receiver's current offset
}
```
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).
## Consequences
**Positive**
- The contract v1 is a complete, implementable surface: every open
shape question from the OQ-04 list is pinned; no placeholder
semantics remain in [core-contract.md](../core-contract.md).
- The tx-seam downcast friction is *dissolved*, not tolerated — the
trait shape makes each engine's handle self-contained, and a future
engine is purely additive (implement the traits; no core edits).
- The taxonomy rule (variants only where callers act) is a durable
principle for later surface additions, keeping queue depth (OQ-05)
from accreting error variants.
- The reserved namespace is exact (`__alkstore_`), testable, and
engine-independent.
**Negative**
- Dyn dispatch on `begin_tx`/mechanism returns means boxed handles and
boxed futures per op — bounded by POC measurement as negligible
against commit costs, but real; monomorphized fast paths are the
recorded optimization if profiles ever demand them.
- Wake payloads are dropped from what the SQLite engine *could* carry
through — engine-side, honker's transport capability is unused by
the contract surface (the cost of an honest uniform contract).
- `commit(self: Box<Self>)` shape means a consumed handle can't be
reused after commit (callers re-`begin_tx`) — matches the
caller-owned lifetime the POCs verified, but is stricter than
honker's re-usable `&Transaction` style.
- OQ-05/OQ-09/OQ-08 remain open: this ADR deliberately does not pin
queue depth, the scheduler shape, or capability flags; the v1
additions to those surfaces will be later contract extensions.
## References
- OQ-04 (`docs/architecture/open-questions.md`) — this ADR's
resolution.
- The honker-rs surface (`/workspace/honker`
`packages/honker-rs/src/lib.rs` v0.5.0) — the starting artifact;
`/workspace/honker` is a reference checkout
([ADR-005](005-dependency-ownership.md) posture).
- `docs/research/poc-sqlite-posture-findings.md` §Probe 1 (trait
friction), §"What feeds where"; `docs/research/poc-pg-posture-findings.md`
§Sub-module T (tx-seam shapes), §Sub-module W (wake/reconnect),
§Payload boundary (`PayloadTooLarge`).
- [core-contract.md](../core-contract.md) — the spec this ADR's
§1–§5 pin into place.
- [ADR-002](002-feature-scope.md) (scope),
[ADR-006](006-wake-and-delivery-contract.md) (wake contract,
namespace existence, guarantee table),
[ADR-007](007-transactional-seam.md) (tx seam mechanics).
- OQ-09 (scheduler row transfer), OQ-08 (capability surface), OQ-10
(versioning discipline for future contract extensions).
+8 -8
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-04
last_updated: 2026-10-05
---
# Deployment
@@ -68,10 +68,11 @@ namespace bullet).
| SQLite | `synchronous` | WAL + `NORMAL` shipped ([ADR-003](decisions/003-sqlite-driver.md)); FULL is available consumer-side for stricter durability; commit fsyncs land at WAL checkpoints (the ~1000-commit spike cadence, POC #1) |
| Postgres | `synchronous_commit` | per-session knob; `on` is ship config (p50 2.40 ms seam); `off` trades max-tail (40.9 ms) for slightly better p50 — measured, honest trade ([ADR-004](decisions/004-postgres-driver.md)); session-level SET mechanics POC-verified |
These are engine-configuration concerns, *not* trait surface (a
consumer may set them via their own engine config — what part of
engine config is contract-level `Store::open` shape vs engine-crate
docs is OQ-04's config-shape item).
These are engine-configuration concerns, *not* trait surface. What
part of engine config is contract-level shape vs engine-crate docs is
decided: constructors and option structs live in the engine crates;
the contract is the trait the constructor returns
([ADR-008](decisions/008-contract-v1-pinning.md) §6).
## Toolchain / platform notes
@@ -105,6 +106,7 @@ From both POCs (single-box, relative shapes are the deliverable —
| [003](decisions/003-sqlite-driver.md) | SQLite driver | bundling, toolchain floor |
| [004](decisions/004-postgres-driver.md) | Postgres driver | listener budget line, forwarder posture |
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | where capability differences may surface |
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | constructor/options in engine crates; no capability surface in v1 (OQ-08) |
## Open Questions
@@ -113,6 +115,4 @@ Open questions are tracked in
questions affecting this document:
- **OQ-08**: capability-surface shape — compile-time vs runtime flags
vs matrix-only (open)
- **OQ-04**: engine-config surface in the contract's `Store::open`
shape (shared) (open)
vs matrix-only (open)
+6 -1
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-04
last_updated: 2026-10-05
---
# Postgres engine
@@ -47,6 +47,10 @@ obligations live in the core spec.
silent), re-LISTEN from the channel list after every reconnect
(exponential backoff 50 ms → 2 s cap), and the synthetic
reconnect-wake on the reserved channel
(`__alkstore_listener_reconnected__`,
[ADR-008](decisions/008-contract-v1-pinning.md) §4) — broadcast to
*every* subscriber's receiver, per the wake contract's
reconnect-recovery semantics
([ADR-006](decisions/006-wake-and-delivery-contract.md)).
- One listener serves N channels and N subscribers; re-attach is a
broadcast re-subscribe (no server round-trips); per-channel
@@ -98,6 +102,7 @@ the record in [ADR-003](decisions/003-sqlite-driver.md).
| [005](decisions/005-dependency-ownership.md) | Ownership | published libs as-is; `postgres-notify` derive-not-adopt |
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | LISTEN push, no replay, synthetic reconnect-wake |
| [007](decisions/007-transactional-seam.md) | Tx seam | direct pooled-object handle, no bridging |
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | pinned surface; reserved reconnect-wake channel string; `PayloadTooLarge` taxonomy variant |
## Open Questions
+2 -1
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-04
last_updated: 2026-10-05
---
# SQLite engine
@@ -101,6 +101,7 @@ consumption stands.
| [005](decisions/005-dependency-ownership.md) | Ownership | published honker-core; named fork triggers |
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | data_version watcher, coalescing, death-closes-receivers |
| [007](decisions/007-transactional-seam.md) | Tx seam | writer-slot lease, `BEGIN IMMEDIATE`, `spawn_blocking` round-trips |
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | pinned surface; outbox backing queue derived under the reserved prefix; wake payload transport unused (non-contract) |
## Open Questions
+38 -39
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-04
last_updated: 2026-10-05
---
# alkstore — Open Questions
@@ -11,10 +11,12 @@ The Phase 0 register's questions (`OQ-ST-01..08` in
mirror OQ-ST-01..08 one-to-one**, keeping their Phase 0 statuses
(a resolved register question stays listed here as resolved, with the
ADR that carries its decision). New Phase 1 questions append from
OQ-09. Suggested resolution order: **OQ-04 first** (the contract
surface everything else hangs off), then OQ-09/OQ-10 (scoped to
OQ-04's outcome), then OQ-05/OQ-08 (OQ-05 has an independent design
track; OQ-08 depends only on the trait's shape, OQ-04).
OQ-09. Resolution order so far: **OQ-04 resolved** (2026-10-05,
[ADR-008](decisions/008-contract-v1-pinning.md) — the contract surface
everything else hangs off). Next: OQ-05/OQ-09 (the queue semantics
track; OQ-09 also owns the scheduler guarantee row), OQ-06
(the SQLite dependency gate), OQ-08 (rides the now-pinned trait
shape), OQ-10 (versioning discipline for contract extensions).
Resolved questions stay listed with their resolution; they are not
deleted.
@@ -78,7 +80,7 @@ deleted.
## Theme: Core contract
### OQ-04: Contract pinning — the exact trait surface and its semantics *(== OQ-ST-04)*
### OQ-04: Contract pinning — the exact trait surface and its semantics *(== OQ-ST-04)* — **RESOLVED**
*(Retitled in promotion: OQ-ST-04's framing — "the reactive
abstraction — what does the unified notify surface look like?" —
@@ -87,38 +89,30 @@ narrowed to the pinning work its own record already scoped.)*
- **Origin**: [core-contract.md](core-contract.md),
[ADR-006](decisions/006-wake-and-delivery-contract.md),
[ADR-007](decisions/007-transactional-seam.md)
- **Status**: open (de-risked; both engine sides POC-verified — this is
paper work over a complete evidence base)
- **Status**: resolved (2026-10-05, Phase 1 — [ADR-008](decisions/008-contract-v1-pinning.md))
- **Priority**: high
- **Resolution**: open. The decisions that bound it are made
([ADR-006](decisions/006-wake-and-delivery-contract.md): opaque-wake + re-read, the delivery-guarantee table, the
reserved namespace's existence; [ADR-007](decisions/007-transactional-seam.md): caller-held `TxHandle`
with `*_tx` methods). What remains to pin, per the honker-rs surface
as starting artifact against the inventory rows:
- Which surface parts are contract v1 and which are engine-extension
(queue depth, scheduler surface, lock semantics details).
- `TxHandle` representation: `dyn` + per-engine downcast (POC
sketch, works, two recorded frictions) vs generic/enum handle.
- The reserved/meta namespace: exact strings (Postgres
reconnect-wake channel like `__listener_reconnected__`), and
whether SQLite-side internal names carry an equivalent prefix.
- Error taxonomy: what callers can match on per mechanism (e.g.
`PayloadTooLarge` is POC-verified pg-side; SQLite's notify has no
8000-byte limit — is the error universal?).
- Naming/grouping of the honker-rs surface where renames clarify
(e.g. what `listen()` returns — a wake receiver? a subscription
handle?).
- The `Store::open` config shape — what engine-configuration
(durability knobs, pool sizing, listener posture) is consumer
surface vs engine crate docs.
- Verification backlog for properties the POCs pinned on one engine
only — e.g. lock TTL/expiry re-acquisition is POC-pinned on
Postgres but rests on honker's machinery on SQLite; the contract
test suite must pin the SQLite side too.
- Delivery-guarantee rows for locks and scheduler (the guarantee
table in [ADR-006](decisions/006-wake-and-delivery-contract.md) covers notify/streams/queues; lock-vs-TTL-race
and scheduler-tick guarantees need either rows there or an
explicit "scheduler is queues" absorption via OQ-09).
- **Resolution**: Pinned as contract v1 by
[ADR-008](decisions/008-contract-v1-pinning.md): (1) the v1
surface partition — notify/streams/queue-skeleton/locks/outbox/tx
seam are contract; queue depth rides OQ-05, scheduler shape rides
OQ-09, capabilities ride OQ-08, `claim_waker` stays engine-internal,
cut-flag rows stay out. (2) `TxHandle`: the `*_tx` methods live on
the handle trait — boxed `dyn` handle, no downcast, no enum (the
enum would invert ADR-001's dependency direction); the POCs'
`as_any_mut` friction dissolves by construction. (3) `listen()`
returns a `WakeReceiver` delivering opaque `Wake { channel }` —
honker's payload transport is not surfaced (uniformity with the
opaque contract, ADR-006). (4) Reserved prefix `__alkstore_`, with
`__alkstore_listener_reconnected__` as the one v1-reserved string
(the POC's ad-hoc `__listener_reconnected__` renamed at
implementation). (5) Error taxonomy: `PayloadTooLarge` /
`ReservedName` / `InvalidName` / `Closed` / `Codec` / `Database`,
pinned by the act-differently rule. (6) `Store::open` is engine-crate
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
backlog) tracks the one-engine-pinned properties.
- **Cross-references**: OQ-01, OQ-10, OQ-09, OQ-08.
## Theme: Queues and scheduling
@@ -155,7 +149,11 @@ narrowed to the pinning work its own record already scoped.)*
inspectable/pausable schedule objects (`add/pause/resume/update/
list/remove`), it stays a first-class mechanism (the full honker
shape). Decided against the inventory rows, not the whole honker
menu.
menu. **Also owns the delivery-guarantee table's scheduler row**
(transferred by [ADR-008](decisions/008-contract-v1-pinning.md) §7:
under collapse the scheduler inherits the queues row; otherwise a
dedicated row is pinned with the surface decision). The scheduler
surface is *not* part of contract v1 ([ADR-008](decisions/008-contract-v1-pinning.md) §1).
- **Cross-references**: OQ-05, OQ-04.
## Theme: Engines and dependencies
@@ -190,7 +188,8 @@ narrowed to the pinning work its own record already scoped.)*
honker's honesty posture, inherited). The unified surface must not
pretend SQLite is multi-host. Options: per-engine capability flags
(`Store::capabilities()`), a documented deployment matrix only
([deployment.md] carries the facts), or compile-time knowledge only
([deployment.md](deployment.md) carries the facts), or compile-time
knowledge only
(a consumer choosing the SQLite engine knows). Rides OQ-04: the
trait's shape constrains where capability differences can surface.
- **Cross-references**: OQ-04, [ADR-006](decisions/006-wake-and-delivery-contract.md).
+3 -2
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-04
last_updated: 2026-10-05
---
# alkstore — Overview
@@ -28,7 +28,7 @@ Per [ADR-001](decisions/001-crate-split.md):
| Crate | Contents | Driver dependencies |
|---|---|---|
| `alkstore` (core) | trait surface, types, error model, capability flags | none |
| `alkstore` (core) | trait surface, types, error model | none (capability flags, if ever, are OQ-08's to add) |
| `alkstore-sqlite` | SQLite engine ([ADR-003](decisions/003-sqlite-driver.md)) | rusqlite, honker-core |
| `alkstore-postgres` | Postgres engine ([ADR-004](decisions/004-postgres-driver.md)) | tokio-postgres, deadpool-postgres |
| (mem engine, optional) | test convenience, decided at implementation ([ADR-001](decisions/001-crate-split.md)) | none |
@@ -71,6 +71,7 @@ Per [ADR-002](decisions/002-feature-scope.md):
| [005](decisions/005-dependency-ownership.md) | Published libraries by default, named fork triggers | Accepted |
| [006](decisions/006-wake-and-delivery-contract.md) | Opaque wake + re-read; notify-vs-streams guarantee split | Accepted |
| [007](decisions/007-transactional-seam.md) | Caller-held `TxHandle` with `*_tx` methods | Accepted |
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 surface pinning (partition, TxHandle shape, wake type, reserved strings, error taxonomy) | Accepted |
## Non-goals
+14 -9
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-04
last_updated: 2026-10-05
---
# Queues, scheduler, outbox — semantics depth
@@ -30,9 +30,11 @@ when made.
- **Wake-driven consumption**: the pg engine's default is LISTEN-driven
claim with a re-poll safety net; SQLite's is watcher wake with
per-subscription fanout ([ADR-006](decisions/006-wake-and-delivery-contract.md)).
- **Job options** (the honker-rs surface, the contract starting
artifact): `delay, priority, max_attempts, expires` at enqueue;
`ack / retry / fail / heartbeat` on the job handle.
- **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` at
enqueue; `ack / retry / fail / heartbeat` on the job handle. Deeper
`QueueOpts`/retry surface is this document's design space.
- **Cut-flag context**: result storage is
[ADR-002](decisions/002-feature-scope.md)'s cut-flag row; if
OQ-05's design shows queue consumers provably need
@@ -95,11 +97,13 @@ when made.
- Schema-scoped DDL (the pg-boss design) vs shared-schema table
naming. Rides the reserved-namespace contract
([ADR-006](decisions/006-wake-and-delivery-contract.md), OQ-04):
queue/stream/lock tables and the reserved channel prefix
must be collision-proof against consumer tables in the same
database (the alkblobs co-tenant-tables precedent — ADR-008 in
`/workspace/@alkdev/alkblobs/docs/architecture/decisions/`).
([ADR-006](decisions/006-wake-and-delivery-contract.md),
[ADR-008](decisions/008-contract-v1-pinning.md) §4):
the reserved *channel* namespace is pinned (`__alkstore_` prefix);
queue/stream/lock **table** layout remains this document's
OQ-05 work and must be collision-proof against consumer tables in
the same database (alkblobs' ADR-008 — co-tenant-tables precedent —
at `/workspace/@alkdev/alkblobs/docs/architecture/decisions/`).
## Reference material
@@ -125,6 +129,7 @@ when made.
| [005](decisions/005-dependency-ownership.md) | Ownership | design-reference postures for the queue family |
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | wake-driven consumption, guarantee table |
| [007](decisions/007-transactional-seam.md) | Tx seam | enqueue_tx commit-atomicity |
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | queue skeleton + EnqueueOpts pinned; depth is OQ-05's extension path |
## Open Questions