ADR-014: transactional outbox enqueue — outbox_enqueue_tx on TxHandle (OQ-13 resolved)

This commit is contained in:
glm-5.3-flash committed 2026-10-05 13:28:39 +00:00
1 parent d401908f13
commit 04a04651dc
8 files changed
+359 -122

No files matched your search

+9 -8
View File
@@ -24,9 +24,9 @@ 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-08, OQ-10, OQ-12, OQ-13 |
| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-06 (resolved), OQ-12, OQ-13 |
| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-05 (resolved), OQ-08, OQ-12, OQ-13 |
| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-08, OQ-10, OQ-12 |
| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-06 (resolved), OQ-12 |
| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-05 (resolved), OQ-08, OQ-12 |
| [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-06 (resolved) |
| [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) | — |
@@ -48,6 +48,7 @@ pending architecture review and OQ resolution.
| [011](decisions/011-sqlite-substrate-fork.md) | SQLite substrate — fork honker-core into owned code | Accepted |
| [012](decisions/012-forked-substrate-design.md) | Forked substrate design — contract-blind boundary, fidelity posture, port deltas | Accepted |
| [013](decisions/013-fold-substrate-into-sqlite.md) | Fold the forked substrate into `alkstore-sqlite` — no fourth crate | Accepted |
| [014](decisions/014-outbox-tx-enqueue.md) | Transactional outbox enqueue — `outbox_enqueue_tx` on the `TxHandle` trait | Accepted |
## Open Questions
@@ -58,10 +59,8 @@ suggested resolution order:
- **OQ-12** (high): streams depth — key semantics, `StreamEvent`
shape, ordering guarantee row, retention (Phase 1 review find;
evidence gathered in the OQ).
- **OQ-13** (high): transactional outbox enqueue shape — the v1
`TxHandle` surface cannot express the outbox's one load-bearing
operation (Phase 1 review find; option set sketched in the OQ).
evidence gathered in the OQ; check consumer rows for a
per-key-ordering need before choosing among its options).
- **OQ-08** (medium): capability-surface shape.
- **OQ-10** (medium): contract versioning across engine crates.
- **OQ-11** (medium): forked-substrate follow-through (provenance
@@ -76,7 +75,9 @@ Resolved (kept with resolutions): OQ-01 (feature scope), OQ-02
scheduler guarantee row pinned), ~~OQ-05~~ (queue semantics depth —
[ADR-010](decisions/010-queue-semantics-depth.md)), ~~OQ-06~~
(honker-core quality read — fork fired,
[ADR-011](decisions/011-sqlite-substrate-fork.md)).
[ADR-011](decisions/011-sqlite-substrate-fork.md)), ~~OQ-13~~
(transactional outbox enqueue shape —
[ADR-014](decisions/014-outbox-tx-enqueue.md)).
No deferred OQs: all open questions are actionable Phase 1 work with
complete evidence bases.
+37 -9
View File
@@ -13,7 +13,9 @@ inventory-confirmed features — [ADR-002](decisions/002-feature-scope.md)
— and pinned against it. The base surface is pinned by
[ADR-008](decisions/008-contract-v1-pinning.md) (contract v1 partition,
`TxHandle` representation, wake type, reserved strings, error
taxonomy, config split); ADR-009/ADR-010 add the first *post-v1
taxonomy, config split — amended in place, pre-implementation, by
[ADR-014](decisions/014-outbox-tx-enqueue.md): `outbox_enqueue_tx`
joins the `TxHandle` trait); ADR-009/ADR-010 add the first *post-v1
contract extensions* (scheduler collapse surface, `QueueOpts` depth —
versioning discipline for such extensions is OQ-10's); the
*obligations* are this document.
@@ -57,10 +59,16 @@ trait TxHandle {
publish_tx(stream, payload) -> event_id
notify_tx(channel, payload)
save_offset_tx(stream, consumer, offset)
outbox_enqueue_tx(outbox, opts, payload) -> job_id
commit(self: Box<Self>) -> Result<()> // or rollback
}
```
`outbox_enqueue_tx` takes the **outbox name** and derives the backing
queue engine-side (the reserved prefix makes the derived name
unreachable by `enqueue_tx` by design —
[ADR-014](decisions/014-outbox-tx-enqueue.md)).
Mechanism handles come off the store (or, for transactional variants,
off the handle):
@@ -68,9 +76,9 @@ off the handle):
store.notify(channel, payload) handle.notify_tx(channel, payload)
store.stream(name) -> Stream handle.publish_tx / save_offset_tx
store.queue(name, opts) -> Queue handle.enqueue_tx
store.outbox(name) -> Outbox handle.outbox_enqueue_tx(outbox, opts, payload)
store.try_lock(name, owner, ttl) -> Option<Lock>
store.listen(channel) -> Box<dyn WakeReceiver>
store.outbox(name) -> Outbox
store.schedule(name, spec, queue, payload, opts) -> Result<Schedule>
store.unschedule(name) -> bool
store.run_schedules(stop) -> Result<()>
@@ -238,9 +246,22 @@ delivery slower than the backing queue's stamped visibility timeout
(default 60 s for outbox-backed queues) can have its claim expire
mid-delivery and be redelivered — the dual-execution window;
idempotent delivery is the consumer's obligation, same as queues.
The transactional *enqueue* side's shape is OQ-13's (the v1 surface
has a real hole there — see
[open-questions.md](open-questions.md)).
The transactional enqueue side is `outbox_enqueue_tx(outbox, opts,
payload)` on the `TxHandle` trait
([ADR-014](decisions/014-outbox-tx-enqueue.md)): it takes the outbox
name (validated like `store.outbox(name)` — empty → `InvalidName`,
reserved-prefixed → `ReservedName`), derives the backing queue name
engine-side, and lands the job row in the caller's transaction with
`EnqueueOpts` stamped per [ADR-010](decisions/010-queue-semantics-
depth.md) §3a over the backing queue's derived `QueueOpts`. Commit
makes the job visible to `run_once` exactly when the business write
commits; rollback drops both (the no-ghosts property,
[ADR-007](decisions/007-transactional-seam.md)). The derived backing
queue name is not expressible through `enqueue_tx` (the reserved
prefix is rejected on directly-supplied names) —
`outbox_enqueue_tx` is the only transactional path into it, which is
itself the guarantee that the derivation cannot be collided with.
### scheduler
@@ -384,6 +405,13 @@ before the engine specs are called `stable`:
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.
- **`outbox_enqueue_tx` commit-atomicity on both engines**
([ADR-014](decisions/014-outbox-tx-enqueue.md)) — rollback drops
the backing-queue job row together with the business write (no
ghost job); commit makes it claimable by `run_once`; reserved/empty
outbox names rejected identically on the tx path
(`ReservedName`/`InvalidName`); stamped opts visible via `get_job`
equal for both engines.
## Design Decisions
@@ -398,6 +426,7 @@ before the engine specs are called `stable`:
| [010](decisions/010-queue-semantics-depth.md) | Queue depth (post-v1 extension) | visibility/renewal, opts stamping, backoff curve, dead-letter, sweep, layout |
| [011](decisions/011-sqlite-substrate-fork.md) | Substrate fork | SQLite substrate owned (`__alkstore_*` naming); queue ops re-derived on contract v1 |
| [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate boundary; engine-side formula arithmetic pinned equivalent by the contract suite |
| [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue (amends 008) | `outbox_enqueue_tx` on `TxHandle`; outbox-name validation; derived backing queue reached only through the outbox surface |
## Open Questions
@@ -410,10 +439,9 @@ questions affecting this document:
- **OQ-08**: capability-surface shape ([open](open-questions.md))
- **OQ-12**: streams depth — key semantics, `StreamEvent` shape,
ordering row, retention ([open](open-questions.md))
- **OQ-13**: transactional outbox enqueue shape
([open](open-questions.md))
Resolved on this document's surface: **OQ-09** (scheduler collapse —
[ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue
[ADR-009](decisions/009-scheduler-collapse.md)), **OQ-05** (queue
semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)),
2026-10-05.
and **OQ-13** (transactional outbox enqueue shape —
[ADR-014](decisions/014-outbox-tx-enqueue.md)), 2026-10-05.
@@ -64,13 +64,17 @@ implement identically):
and `release`.
- outbox helper — `outbox(name)` with `enqueue` + `run_once` delivery
worker. *(Depth annotation 2026-10-05: the transactional *enqueue*
shape on the `TxHandle` seam is OQ-13's — the v1 method list has no
outbox enqueue method and the derived backing queue's reserved
prefix makes plain `enqueue_tx` illegal; see
[open-questions.md](../open-questions.md). `run_once` worker
semantics are pinned in core-contract.md's outbox section.)*
shape was OQ-13's — this §1/§2 method list had no outbox enqueue
method and the derived backing queue's reserved prefix made plain
`enqueue_tx` illegal; resolved by
[ADR-014](014-outbox-tx-enqueue.md) — `outbox_enqueue_tx` joins the
`TxHandle` trait. `run_once` worker semantics are pinned in
core-contract.md's outbox section.)*
- the tx seam — `begin_tx` / commit / rollback and the `*_tx` methods
([ADR-007](007-transactional-seam.md)).
([ADR-007](007-transactional-seam.md)). *(Amended 2026-10-05 by
[ADR-014](014-outbox-tx-enqueue.md):
`outbox_enqueue_tx(outbox, opts, payload)` added to the
`TxHandle` trait.)*
- `subscribe(consumer) -> Box<dyn EventReceiver>` — the durable
stream subscription handle, delivering `StreamEvent`s with an
explicit `save_offset` on the receiver (no auto-checkpoint;
@@ -113,6 +117,7 @@ trait TxHandle {
publish_tx(stream, payload) -> event_id
notify_tx(channel, payload)
save_offset_tx(stream, consumer, offset)
outbox_enqueue_tx(outbox, opts, payload) -> job_id // ADR-014
commit(self: Box<Self>) -> Result<()> // or rollback
}
```
@@ -195,7 +200,8 @@ struct Wake { channel: String }
- **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.
stream, queue, lock, and outbox names on every engine *(outbox names
added to the kinds list by [ADR-014](014-outbox-tx-enqueue.md))*.
- **The one v1-reserved string:**
`__alkstore_listener_reconnected__` — the Postgres forwarder's
synthetic reconnect-wake channel ([ADR-004](004-postgres-driver.md)).
@@ -0,0 +1,195 @@
# ADR-014: Transactional outbox enqueue — `outbox_enqueue_tx` on the `TxHandle` trait
## Status
Accepted (2026-10-05, Phase 1 — OQ-13's resolution; amends
[ADR-008](008-contract-v1-pinning.md) §1's outbox and tx-seam bullets
and §2's method list in place, pre-implementation)
## Context
The outbox is "enqueue inside the business transaction + delivery
workers" ([ADR-002](002-feature-scope.md)) — the commit-atomic enqueue
is the mechanism's *only reason to exist*; the delivery side (`run_once`
pull semantics, the backing queue's derived `QueueOpts`) is pinned
(core-contract.md outbox section;
[ADR-010](010-queue-semantics-depth.md) §3/§3a). But the enqueue side
has no method. The hole, stated precisely:
1. The derived backing queue name `__alkstore_outbox:{name}`
([ADR-008](008-contract-v1-pinning.md) §4) carries the reserved
prefix, and every name-bearing entry point *and its `*_tx`
counterpart* rejects that prefix with `ReservedName` — so
`handle.enqueue_tx("__alkstore_outbox:x", …)` is contract-illegal
by construction.
2. `Outbox` (what `store.outbox(name)` returns) is not in the
`TxHandle` method set ([ADR-008](008-contract-v1-pinning.md) §1,
§2), and `TxHandle` has no outbox method of any name.
So as pinned by [ADR-008](008-contract-v1-pinning.md), a consumer can
open a transaction, write business rows, and have *no way* to put an
outbox send inside it — the load-bearing property
([ADR-007](007-transactional-seam.md), guiding principle 2) is
unreachable for this mechanism. Honker solves the same need with a
different seam: `outbox.enqueue_tx(&tx, payload, opts)` routes through
the caller's raw `Transaction`
(`/workspace/honker @ f4e53c6`, `packages/honker-rs/src/lib.rs:506-513`)
— a seam our handle design deliberately does not have: the handle *is*
the tx, opaque, no downcast, no driver types in the contract
([ADR-007](007-transactional-seam.md);
[ADR-008](008-contract-v1-pinning.md) §2).
The evidence base is complete — the contract surfaces (both ADRs), the
honker source (read at revision), the reserved-namespace rules, and
[ADR-010](010-queue-semantics-depth.md) §3a's stamping resolution. No
POC is needed: this is a contract-surface gap, not a semantics debate.
## Decision
### 1. `outbox_enqueue_tx(outbox, opts, payload) -> job_id` on the `TxHandle` trait
One method, added to the `TxHandle` trait:
```text
trait TxHandle {
enqueue_tx(queue, opts, payload) -> job_id
publish_tx(stream, payload) -> event_id
notify_tx(channel, payload)
save_offset_tx(stream, consumer, offset)
outbox_enqueue_tx(outbox, opts, payload) -> job_id // the addition
commit(self: Box<Self>) -> Result<()> // or rollback
}
```
- **The method takes the outbox name, not the derived queue name.**
The engine validates the outbox name (non-empty → else
`InvalidName`; reserved-prefix → else `ReservedName` — the same
entry-point validation `store.outbox(name)` applies, per
[ADR-008](008-contract-v1-pinning.md) §4's rule that `*_tx`
counterparts validate identically) and derives the backing queue
name `__alkstore_outbox:{outbox}` *engine-side*. No prior
`store.outbox(name)` handle is required — outboxes are names, not
registered objects (the queues posture). The outbox name is used
verbatim in the derivation; no further charset/length rule is
pinned in v1 (both engines treat the derived value as an opaque
storage key). The reserved-prefix
rejection is not bypassed: it governs *directly-supplied* names in
the queue/channel/stream/lock namespaces, while the name this method
takes is an **outbox name** — a distinct name kind whose derived
name happens to land under the prefix. The derivation is legitimate
engine-internal naming, exactly as the auto-commit path's
`run_once` consumption of the same backing queue already is.
- **Opts carry `EnqueueOpts` whole** — same type, same position
(name, opts, payload) as `enqueue_tx`. Rationale: ADR-010 §3a makes
enqueue the stamping point — `EnqueueOpts.max_attempts` is the one
per-job override, resolved over the backing queue's derived
`QueueOpts` (visibility 60 s, max_attempts 5, backoff base 5 s;
[ADR-010](010-queue-semantics-depth.md) §3) — and the auto-commit
sibling `outbox.enqueue(payload, opts)` carries opts (honker
parity). A tx form that dropped opts would be asymmetric with its
own auto-commit twin for no gain; and stamping is what makes the
job's behavior inspectable via `get_job` no matter which form
enqueued it.
- **The property delivered**: the outbox send sits in the caller's
transaction — commit makes the backing-queue job row visible to
`run_once`/delivery exactly when the business write commits;
rollback drops both (the no-ghosts property,
[ADR-007](007-transactional-seam.md)). Transactional
local-adjacency (guiding principle 2) is restored for the outbox —
the mechanism exists to carry it.
### 2. Why not an `OutboxHandle` off the tx handle — option (b)
`handle.outbox(name) -> OutboxHandle` (with enqueue) mirrors the
store-side handle pattern, but it costs a second boxed mechanism
handle carried across the transaction to deliver exactly one needed
operation. Every new engine implements another trait for one method;
the "no downcast, no extra hops" rationale of
[ADR-008](008-contract-v1-pinning.md) §2 applies with force — a
one-method handle is surface weight, not symmetry. Option (a) is the
smallest surface that restores the property.
### 3. Explicit rejections
- **Honker's raw-`Transaction` seam is not inherited.** There is no
engine-neutral `Transaction` type to put in a core-crate signature —
rusqlite's `Transaction` and tokio-postgres's client are different
driver types — so routing through one would force either a core
dependency on engine types (inverting
[ADR-001](001-crate-split.md)'s dependency direction) or a
per-engine downcast seam (exactly what
[ADR-008](008-contract-v1-pinning.md) §2 dissolved). Honker's shape
is evidence that the *property* must exist, not that this *seam*
should.
- **Non-transactional-only (option (c)) is rejected outright.**
`outbox.enqueue` auto-commit alone drops the commit-atomic property
— the outbox would degrade to a queues alias, defeating its scope
row's purpose. Listed to be rejected, as the OQ required.
### 4. Amendment framing and verification
- **This completes contract v1 in place, pre-implementation.** No
artifact is released (no crate exists), so no versioning event
occurs — OQ-10's discipline is untouched by this; the method joins
the v1 surface partition's tx-seam bullet and
[core-contract.md](../core-contract.md)'s seam. ADR-008's outbox
bullet carries a pointer annotation, mirroring how ADR-010 §8's
SQLite half recorded its supersession.
- **Contract-suite row**: `outbox_enqueue_tx` commit-atomicity on both
engines — rollback drops the backing-queue job row with the business
write (no ghost job); commit makes it claimable by `run_once`;
reserved/empty outbox names rejected identically on the tx path
(`ReservedName`/`InvalidName`); the stamped opts visible via
`get_job` identical across engines (added to core-contract.md's
verification backlog).
## Consequences
**Positive**
- The outbox's scope row
([ADR-002](002-feature-scope.md)) becomes implementable: both halves
(commit-atomic enqueue, `run_once` delivery) now have pinned
surfaces — decomposition off the current text no longer produces an
unperformable core operation.
- Smallest possible addition: one method, one existing opts type, one
job-id return — no new handle type, no new error variant (the
validation failures reuse the pinned taxonomy,
[ADR-008](008-contract-v1-pinning.md) §5).
- New engines stay purely additive: one more trait method, same
implementation pattern as the other `*_tx` ops.
**Negative**
- `TxHandle` grows by one engine-implemented method — the trait cost
every engine inherits even if it never uses the outbox (the same
per-method cost the rest of the seam carries; measured negligible
against commit costs, ADR-008 §2).
- A mild surface asymmetry: the tx handle enqueues into queues *by
queue name* but into outboxes *by outbox name* (the derived backing
queue is unreachable by `enqueue_tx` by design). This is the
reserved-namespace rule doing its job, but it is a thing consumer
documentation must state plainly: **there is no way to enqueue into
the backing queue except through the outbox surface** (auto-commit
`outbox.enqueue` or tx `outbox_enqueue_tx`) — itself the guarantee
that the derivation cannot be collided with.
## References
- OQ-13 (`docs/architecture/open-questions.md`) — this ADR's
resolution; the evidence and option set sketched there.
- [ADR-008](008-contract-v1-pinning.md) §1 (outbox bullet, annotated),
§2 (`TxHandle` shape — the method joins its list), §4 (reserved
namespace — the rejection this method is designed around), §5 (error
taxonomy — reused, not extended).
- [ADR-007](007-transactional-seam.md) — the seam whose property this
method restores for the outbox; the no-ghosts property.
- [ADR-010](010-queue-semantics-depth.md) §3/§3a — the backing queue's
derived opts and the enqueue-time stamping this method's `EnqueueOpts`
rides.
- [ADR-002](002-feature-scope.md) — the outbox scope row whose
enqueue half this completes.
- Honker's seam (`/workspace/honker` `packages/honker-rs/src/lib.rs`
`Outbox::enqueue_tx` @ f4e53c6) — the reference checkout's raw-
`Transaction` shape, read and rejected (§3).
- OQ-10 — unaffected (amendment framing, §4).
+7 -2
View File
@@ -109,6 +109,7 @@ the record in [ADR-003](decisions/003-sqlite-driver.md).
| [009](decisions/009-scheduler-collapse.md) | Scheduler collapse | schedule rows in the engine schema, re-derived tick, row-locked fire tx, `__alkstore_scheduler` leadership |
| [010](decisions/010-queue-semantics-depth.md) | Queue depth | job-stamped opts, equal-jitter backoff, dead-letter move, no-stranded-rows sweep, one engine-owned schema |
| [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate (SQLite side); pg engine owns its own curve/stamps arithmetic, equivalence pinned by the contract suite |
| [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue | `outbox_enqueue_tx` on `TxHandle`; derivation engine-side inside the caller's tx |
## Open Questions
@@ -121,8 +122,12 @@ questions affecting this document:
- **OQ-12**: streams depth (affects this engine's stream realization
— event-shape/ordering/retention equivalents on the pg side)
([open](open-questions.md))
- **OQ-13**: transactional outbox enqueue shape (affects this
engine's `TxHandle` impl) ([open](open-questions.md))
- **OQ-13**: transactional outbox enqueue shape — **resolved**
(2026-10-05, [ADR-014](decisions/014-outbox-tx-enqueue.md)):
`outbox_enqueue_tx(outbox, opts, payload)` on the `TxHandle` trait;
the pg engine validates the outbox name and derives
`__alkstore_outbox:{name}` engine-side inside the caller's
transaction.
Resolved: **OQ-09** (scheduler collapse —
[ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue
+7 -2
View File
@@ -125,6 +125,7 @@ family is `__alkstore_*` (ADR-010 §8's naming authorization).
| [011](decisions/011-sqlite-substrate-fork.md) | Substrate fork | OQ-06's trigger fired; substrate owned, queue ops re-derived on contract v1 |
| [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate, fidelity posture, port deltas, panic-free watcher death |
| [013](decisions/013-fold-substrate-into-sqlite.md) | Substrate packaging | folded into `alkstore-sqlite` (`src/substrate/`); no fourth crate |
| [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue | `outbox_enqueue_tx` on `TxHandle`; derivation engine-side through the writer-slot lease |
## Open Questions
@@ -137,8 +138,12 @@ questions affecting this document:
[ADR-011](decisions/011-sqlite-substrate-fork.md); trigger fired).
- **OQ-12**: streams depth (affects this engine's stream realization
— key column, event shape, retention) ([open](open-questions.md))
- **OQ-13**: transactional outbox enqueue shape (affects this
engine's `TxHandle` impl) ([open](open-questions.md))
- **OQ-13**: transactional outbox enqueue shape — **resolved**
(2026-10-05, [ADR-014](decisions/014-outbox-tx-enqueue.md)):
`outbox_enqueue_tx(outbox, opts, payload)` on the `TxHandle` trait;
the SQLite handle routes the op through the writer-slot lease
([ADR-007](decisions/007-transactional-seam.md)) into the forked
substrate's `__alkstore_outbox:{name}` backing queue.
Resolved: **OQ-09** (scheduler collapse —
[ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue
+86 -93
View File
@@ -21,18 +21,19 @@ boundaries, fidelity posture, port deltas; its substrate-side residue
is OQ-11); **the fork packaging folded** (2026-10-05,
[ADR-013](decisions/013-fold-substrate-into-sqlite.md) — no
`alkstore-substrate` crate; the forked machinery is a module subtree of
`alkstore-sqlite`). Next: OQ-08 (rides the now-pinned trait shape and the
ADR-011 substrate fork), OQ-10 (versioning discipline for contract
extensions; note ADR-011 changes its substrate-side facts for SQLite —
the forked machinery lives in-tree inside the engine crate per
[ADR-013](decisions/013-fold-substrate-into-sqlite.md), so the
engine/core contract pairing is what the discipline must track), OQ-11
(fork follow-through items — substrate-side, non-consumer-facing),
**OQ-12** (streams depth — a Phase 1 review find: key semantics,
`StreamEvent` shape, ordering row, retention; evidence gathered), and
**OQ-13** (the transactional outbox enqueue shape — a Phase 1 review
find: the v1 `TxHandle` surface cannot express the outbox's one
load-bearing operation).
`alkstore-sqlite`); **OQ-13 resolved** (2026-10-05,
[ADR-014](decisions/014-outbox-tx-enqueue.md) —
`outbox_enqueue_tx` joins the `TxHandle` trait, the hole is closed).
Next: **OQ-12** (streams depth — a Phase 1 review
find: key semantics, `StreamEvent` shape, ordering row, retention;
evidence gathered; check consumer rows for per-key-ordering need
before choosing among its options), OQ-08 (rides the now-pinned trait
shape and the ADR-011 substrate fork), OQ-10 (versioning discipline
for contract extensions; note ADR-011 changes its substrate-side facts
for SQLite — the forked machinery lives in-tree inside the engine
crate per [ADR-013](decisions/013-fold-substrate-into-sqlite.md), so
the engine/core contract pairing is what the discipline must track),
OQ-11 (fork follow-through items — substrate-side, non-consumer-facing).
Resolved questions stay listed with their resolution; they are not
deleted.
@@ -127,9 +128,10 @@ 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, OQ-09, OQ-08.
- **Cross-references**: OQ-01, OQ-10, OQ-09, OQ-08; OQ-13 (the one
gap the v1 pinning's own Phase 1 review found in it).
## Theme: Queues and scheduling
@@ -232,42 +234,6 @@ narrowed to the pinning work its own record already scoped.)*
[ADR-011](decisions/011-sqlite-substrate-fork.md); ADR-005's posture
calculus applied, not renegotiated.
## Theme: Deployment and capabilities
### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)*
- **Origin**: [deployment.md](deployment.md)
- **Status**: open
- **Priority**: medium
- **Resolution**: open. The pg engine is natively multi-host (POC #2
verified — no single-host assumption to remove); SQLite is
single-machine by nature (file-backed, NFS-two-writers unsupported —
honker's honesty posture, inherited by the forked substrate). The
unified surface must not pretend SQLite is multi-host. Options:
per-engine capability flags (`Store::capabilities()`), a documented
deployment matrix only ([deployment.md](deployment.md) carries the
facts), or compile-time knowledge only (a consumer choosing the
SQLite engine knows). Rides the now-pinned contract shape
([ADR-008](decisions/008-contract-v1-pinning.md)): the trait
constrains where capability differences can surface.
- **Cross-references**: OQ-04, [ADR-006](decisions/006-wake-and-delivery-contract.md), [ADR-012](decisions/012-forked-substrate-design.md) (the substrate inherits the honesty posture).
### OQ-10: How do engine crates track core-contract version changes?
- **Origin**: [overview.md](overview.md),
[ADR-001](decisions/001-crate-split.md)
- **Status**: open
- **Priority**: medium
- **Resolution**: open. The core crate's trait surface is a contract
the engine crates must track ([ADR-001](decisions/001-crate-split.md) negative consequence). What
is the versioning/sync discipline — semver-bump-only-when-
contract-changes, engines pin core ranges, a contract-compatibility
test suite the engines run against the core's trait definitions?
What happens to a released engine crate when core makes a contract
breaking change?
- **Cross-references**: OQ-04 (the contract being versioned — now
including its first post-v1 extensions, ADR-009/ADR-010), OQ-02.
### OQ-11: Forked-substrate follow-through — scaffold, provenance register, and cherry-pick discipline
- **Origin**: [ADR-012](decisions/012-forked-substrate-design.md)
@@ -302,6 +268,46 @@ narrowed to the pinning work its own record already scoped.)*
carries the fold's provenance duties in-tree), ADR-011, ADR-012,
ADR-013.
### OQ-10: How do engine crates track core-contract version changes?
- **Origin**: [overview.md](overview.md),
[ADR-001](decisions/001-crate-split.md)
- **Status**: open
- **Priority**: medium
- **Resolution**: open. The core crate's trait surface is a contract
the engine crates must track ([ADR-001](decisions/001-crate-split.md) negative consequence). What
is the versioning/sync discipline — semver-bump-only-when-
contract-changes, engines pin core ranges, a contract-compatibility
test suite the engines run against the core's trait definitions?
What happens to a released engine crate when core makes a contract
breaking change?
- **Cross-references**: OQ-04 (the contract being versioned — now
including its first post-v1 extensions, ADR-009/ADR-010), OQ-02.
## Theme: Deployment and capabilities
### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)*
- **Origin**: [deployment.md](deployment.md)
- **Status**: open
- **Priority**: medium
- **Resolution**: open. The pg engine is natively multi-host (POC #2
verified — no single-host assumption to remove); SQLite is
single-machine by nature (file-backed, NFS-two-writers unsupported —
honker's honesty posture, inherited by the forked substrate). The
unified surface must not pretend SQLite is multi-host. Options:
per-engine capability flags (`Store::capabilities()`), a documented
deployment matrix only ([deployment.md](deployment.md) carries the
facts), or compile-time knowledge only (a consumer choosing the
SQLite engine knows). Rides the now-pinned contract shape
([ADR-008](decisions/008-contract-v1-pinning.md)): the trait
constrains where capability differences can surface.
- **Cross-references**: OQ-04, [ADR-006](decisions/006-wake-and-delivery-contract.md), [ADR-012](decisions/012-forked-substrate-design.md) (the substrate inherits the honesty posture).
## Theme: Core contract (Phase 1 review finds)
### OQ-12: Streams depth — key semantics, `StreamEvent` shape, ordering row, retention
- **Origin**: [core-contract.md](core-contract.md) streams section,
@@ -356,60 +362,47 @@ narrowed to the pinning work its own record already scoped.)*
open), OQ-01 (scope row), ADR-002, ADR-006 (the guarantee table
the ordering row extends), ADR-008 §1.
### OQ-13: Transactional outbox enqueue shape — the `TxHandle` surface cannot express it
### OQ-13: Transactional outbox enqueue shape — the `TxHandle` surface cannot express it — **RESOLVED**
- **Origin**: [core-contract.md](core-contract.md) (outbox section),
[ADR-008](decisions/008-contract-v1-pinning.md) §1 (Phase 1
architecture review, 2026-10-05)
- **Status**: open
- **Status**: resolved (2026-10-05, Phase 1 —
[ADR-014](decisions/014-outbox-tx-enqueue.md))
- **Priority**: high (the outbox's *only reason to exist* is the
commit-atomic enqueue; as pinned, the v1 surface cannot perform it
— a decomposition off the current text would produce an
unimplementable task)
- **Resolution**: open. The hole, stated precisely: the outbox is
"enqueue inside the business transaction + delivery workers"
([ADR-002](decisions/002-feature-scope.md)); the delivery side is
pinned (`run_once`, plus plain queue consumption of the backing
queue), but the enqueue side has no method. The derived backing
queue name `__alkstore_outbox:{name}`
([ADR-008](decisions/008-contract-v1-pinning.md) §4) is
reserved-prefix — every entry point **rejects** it, so
`handle.enqueue_tx("__alkstore_outbox:x", …)` is contract-illegal
by construction; and `Outbox` (the handle `store.outbox(name)`
returns) is not in the `TxHandle` method set
([ADR-008](decisions/008-contract-v1-pinning.md) §1, §2), so there
is no `outbox_enqueue_tx` either. Honker solves it with a
different seam: `outbox.enqueue_tx(&tx, …)` routes through the
caller's raw `Transaction`
(`packages/honker-rs/src/lib.rs:506-513`) — a seam our handle
design ([ADR-007](decisions/007-transactional-seam.md),
[ADR-008](decisions/008-contract-v1-pinning.md) §2 — the handle
*is* the tx, no downcast) deliberately does not have. Options
(starter set, not exhaustive):
(a) `outbox_enqueue_tx(name, payload)` on the `TxHandle` trait —
symmetric with the other `*_tx` methods, one method, engine resolves
the derived name on the engine side (bypasses the entry-point
rejection legitimately, since the *method* takes the outbox name,
not the derived queue name);
(b) an `OutboxHandle` obtained from the `TxHandle`
(`handle.outbox(name) -> OutboxHandle` with enqueue) — more
symmetrical with the store-side handle, two hops;
(c) non-transactional only — `outbox.enqueue` auto-commit, dropping
the commit-atomic property (defeats the purpose; listed to be
rejected explicitly).
Decision rule: smallest surface that restores the
transactional-local-adjacency property (guiding principle 2) for
the outbox without opening the reserved namespace; (a) reads
strongest against ADR-008 §2's rationale and needs one contract
suite row.
- **Cross-references**: OQ-04 (the pinning that missed this),
OQ-12 (the sibling depth gap found by the same review), ADR-002
- **Resolution**: Pinned by
[ADR-014](decisions/014-outbox-tx-enqueue.md): one method,
`outbox_enqueue_tx(outbox, opts, payload) -> job_id`, added to the
`TxHandle` trait. The method takes the **outbox name** (validated
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` —
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
engine-internal naming (the same status `run_once`'s consumption of
the backing queue already had). Option (b) (an `OutboxHandle` off
the tx handle) is rejected — a second boxed handle to deliver one
op; option (c) (non-transactional only) is rejected — it drops the
property the mechanism exists for. Honker's raw-`Transaction` seam
is not inherited (no engine-neutral `Transaction` type exists; it
would force a core→engine dependency or a downcast seam — the two
things ADR-008 §2 dissolved). Framed as completing contract v1 in
place, pre-implementation — no versioning event, OQ-10 untouched.
Contract-suite row added (commit-atomicity on both engines).
- **Cross-references**: OQ-04 (the pinning that missed this), OQ-12
(the sibling depth gap found by the same review), ADR-002
(outbox scope row), ADR-007/ADR-008 (the seam design), ADR-010 §3
(the outbox's derived QueueOpts — unchanged by this).
## Deferred / Blocked
None currently. Every open OQ above is actionable Phase 1 work
(streams depth, outbox enqueue shape, capability-surface shape,
versioning discipline, fork-scaffold follow-through) with its evidence
base complete — no external arrivals are being waited on.
(streams depth, capability-surface shape, versioning discipline,
fork-scaffold follow-through) with its evidence base complete — no
external arrivals are being waited on.
+5 -1
View File
@@ -24,7 +24,10 @@ made under; the ADRs carry the WHY.
[ADR-009](decisions/009-scheduler-collapse.md)).
- **Transactional enqueue**: commit-atomic with the caller's business
write on both engines ([ADR-007](decisions/007-transactional-seam.md));
rollback drops the job row with no ghosts.
rollback drops the job row with no ghosts. For the outbox's backing
queue the tx path is `outbox_enqueue_tx` (the derived reserved names
are unreachable by `enqueue_tx` —
[ADR-014](decisions/014-outbox-tx-enqueue.md)).
- **Exactly-once claim**: `FOR UPDATE SKIP LOCKED` (pg, POC-pinned
under 4×4 concurrency) / the forked substrate's single-statement
claim (SQLite, re-derived on [ADR-010](decisions/010-queue-semantics-depth.md)
@@ -260,6 +263,7 @@ Collapsed into queues: no `Scheduler` handle, no schedule objects.
| [010](decisions/010-queue-semantics-depth.md) | Queue depth | visibility/heartbeat rules, backoff curve, dead-letter, no-stranded-rows sweep, schema layout |
| [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 |
## Open Questions