docs: Phase 1 review round — wake wording honesty, job-handle validity predicate, outbox/streams depth OQs

- C2: 'at-least-once wake delivery' collapsed to 'best-effort hints'
  (ADR-006 §1 + core-contract) — coalescing and the pg no-replay hole
  contradict per-commit wake promises; the guarantee table's row was
  already correct.
- W2: uniform job-handle validity predicate pinned (ADR-010 §2,
  core-contract, queues.md; verification-backlog row): processing
  state + unexpired deadline; D-12's missing-check not inherited.
- W1: outbox run_once worker semantics pinned in core-contract
  (pull op, ack/retry-on-curve, no heartbeat in delivery — honker
  parity, dual-execution window documented).
- W3: named-locks tx-seam posture stated (no lock_tx; acquisition is
  auto-commit; TTL discipline governs).
- C1 -> OQ-12: streams depth (key semantics w/ honker ground, event
  shape, ordering row, retention); method names pinned, depth open.
- New find -> OQ-13: the v1 TxHandle surface cannot express the
  transactional outbox enqueue (derived backing-queue name is
  reserved-prefix-rejected; honker's raw-Transaction seam unavailable);
  option set + decision rule sketched.
- README resolution order refreshed (OQ-06 resolved); ScheduleOpts +
  stream consumption-trigger lines added to core-contract.
This commit is contained in:
glm-5.3-flash committed 2026-10-05 12:50:33 +00:00
1 parent 8e68b44194
commit d401908f13
9 files changed
+250 -35

No files matched your search

+64 -11
View File
@@ -108,9 +108,12 @@ never sees it.
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.**
Wakes are **best-effort hints, not per-commit delivery promises**:
possibly coalesced (SQLite) or per-notify (Postgres), possibly
repeated after reconnect, possibly absent entirely (SQLite burst
coalescing; the pg no-replay hole) — recovery is the consumer's
re-read plus the engine's reconnect surface, never a guaranteed
deliver. **Consumers must be idempotent on wake.**
- Failure surfaces as channel events, never silence: watcher death
closes the receiver (`recv() -> None`, SQLite); the synthetic
reconnect-wake (a reserved channel
@@ -130,7 +133,11 @@ transaction-aware (`save_offset_tx` gives exactly-once-within-a-
business-tx shape).
- `publish` / `publish_tx` / `publish_with_key` — append to the
stream's durable log.
stream's durable log. *(Depth annotation: the key's semantics, the
`StreamEvent` shape, the ordering row, and log retention are
OQ-12's — method names pinned by
[ADR-008](decisions/008-contract-v1-pinning.md) §1, depth deferred
there; see [open-questions.md](open-questions.md).)*
- `read_since` / `read_from_consumer(offset)` — cursor-based reads;
`get_offset(consumer)` — checkpoint inspection.
- `save_offset` / `save_offset_tx` — consumer checkpoint, explicit;
@@ -142,6 +149,13 @@ business-tx shape).
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).
Consumption is wake-driven with table re-read — the same
mechanism split as queues (durable row, LISTEN/watcher wake,
[ADR-006](decisions/006-wake-and-delivery-contract.md)); the
receiver's engine-side trigger is engine-internal, but events
never require polling to become visible (the queues row's pg
re-poll safety net is an admission of LISTEN loss, not the
posture).
### queues
@@ -165,7 +179,14 @@ obligations:
attempt**; `retry(err, None)` computes the queue's equal-jitter
exponential delay (range pinned by
[ADR-010](decisions/010-queue-semantics-depth.md) §3), `retry(err,
Some(d))` overrides; `fail` = immediate dead-letter. Dead letters:
Some(d))` overrides; `fail` = immediate dead-letter. Handle-op
validity (uniform predicate): an op succeeds only while the row is
`processing` and the caller's claim deadline is unexpired — deadline
lapse refuses all handle ops (heartbeats, acks included) and a
reclaim does too; the dual-execution window's worker may still
complete its work but its ack will not land (at-least-once, as
documented); false, not error
([ADR-010](decisions/010-queue-semantics-depth.md) §2). Dead letters:
move-to-dead storage, `get_job` sees dead rows (with
`last_error`/`died_at`), retention via `dead_letter_retention_s`
(default forever), no redrive API
@@ -181,7 +202,13 @@ obligations:
### named locks
TTL-bounded coordination locks, transactional-friendly:
TTL-bounded coordination locks. Lock rows live in the same database
as the caller's business data — coordination and data co-locate —
but lock ops are **not** part of the tx seam: there is no `lock_tx`;
a lock held across a business transaction is held by explicit
acquisition before/inside it, not transaction-scoped (acquisition is
a separate auto-commit op; rollback of the business tx does not
release it — the TTL discipline governs).
- `try_lock(name, owner, ttl)` — acquire or fail (`Option<Lock>` —
no-work is a value, not an error); `renew` and `release` on the
@@ -201,7 +228,19 @@ TTL-bounded coordination locks, transactional-friendly:
A helper over queues, not a separate mechanism: enqueue inside the
business transaction + the delivery/consumption worker entry points.
Same evidence base and guarantee as queues
([ADR-002](decisions/002-feature-scope.md)).
([ADR-002](decisions/002-feature-scope.md)). Worker semantics
(honker's `run_once` shape, inherited as the pinned posture):
`run_once(worker_id, delivery)` is a *pull* op — claim one job, run
the delivery closure, `ack` on `Ok`, `retry(err, None)` (the queue's
curve) on `Err`; the consumer calls it in its own loop. **No
heartbeat inside delivery** (honker parity, inherited deliberately):
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)).
### scheduler
@@ -213,9 +252,12 @@ the reserved name `__alkstore_scheduler`). Schedules never fire
without a runner (no ambient timers). v1 spec grammar:
`@every <n><unit>` only (`s|m|h|d`); cron strings are rejected
(`InvalidSpec`) pending a consumer-inventory row naming wall-clock
cron. Boundary guarantee:
[ADR-009](decisions/009-scheduler-collapse.md) §4 (at-least-once per
elapsed boundary, bounded catch-up with skip-forward past the cap).
cron. Each boundary fire enqueues ordinary work stamped with
`ScheduleOpts { priority, max_attempts, expires }`
([ADR-009](decisions/009-scheduler-collapse.md) §3). Boundary
guarantee: [ADR-009](decisions/009-scheduler-collapse.md) §4
(at-least-once per elapsed boundary, bounded catch-up with
skip-forward past the cap).
## Cross-cutting contracts
@@ -228,7 +270,7 @@ of record for notify/streams/queues; the locks row is
row is [ADR-009](decisions/009-scheduler-collapse.md) §4's. This spec
inherits them and adds the consumer obligations:
- wake idempotence (notify's at-least-once, coalescing behavior);
- wake idempotence (notify's best-effort, coalescing behavior);
- explicit offset saves (streams);
- visibility-timeout budgeting — heartbeat inside the deadline for
long work; the dual-execution window and reclaim-eats-attempt rules
@@ -335,6 +377,13 @@ before the engine specs are called `stable`:
per [ADR-012](decisions/012-forked-substrate-design.md) §2's
contract-blind boundary; the contract suite must pin both engines'
arithmetic to identical outputs.
- **Job-handle validity predicate on both engines** — the uniform
processing-state + unexpired-deadline rule
([ADR-010](decisions/010-queue-semantics-depth.md) §2): the
late-heartbeat boundary (refused exactly when the deadline has
lapsed), the post-lapse-ack-refusal (at-least-once), and the
ack-vs-reclaim race (one wins atomically); both engines' SQL pin
identical outcomes in the contract suite.
## Design Decisions
@@ -359,6 +408,10 @@ questions affecting this document:
- **OQ-10**: contract versioning discipline across engine crates
([open](open-questions.md))
- **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