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

+14 -10
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 |
| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-05, OQ-06, OQ-09 (all resolved) |
| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-05 (resolved), OQ-08, OQ-09 (resolved) |
| [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 |
| [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) | — |
@@ -53,13 +53,15 @@ pending architecture review and OQ resolution.
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, OQ-09, OQ-05, OQ-06 resolved):
OQ-ST-NN — with new Phase 1 questions appended after). Open, in
suggested resolution order:
- **OQ-06** (high): honker-core quality read — **resolved**
(2026-10-05, [ADR-011](decisions/011-sqlite-substrate-fork.md)): the
fork trigger fired; SQLite substrate is owned code forked from
honker-core's lineage, queue ops re-derived on contract v1.
- **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).
- **OQ-08** (medium): capability-surface shape.
- **OQ-10** (medium): contract versioning across engine crates.
- **OQ-11** (medium): forked-substrate follow-through (provenance
@@ -72,7 +74,9 @@ Resolved (kept with resolutions): OQ-01 (feature scope), OQ-02
[ADR-008](decisions/008-contract-v1-pinning.md)), ~~OQ-09~~
(scheduler collapse — [ADR-009](decisions/009-scheduler-collapse.md);
scheduler guarantee row pinned), ~~OQ-05~~ (queue semantics depth —
[ADR-010](decisions/010-queue-semantics-depth.md)).
[ADR-010](decisions/010-queue-semantics-depth.md)), ~~OQ-06~~
(honker-core quality read — fork fired,
[ADR-011](decisions/011-sqlite-substrate-fork.md)).
No deferred OQs: all open questions are actionable Phase 1 work with
complete evidence bases.
+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
@@ -42,9 +42,13 @@ a cache invalidates on wake and re-reads, instead of re-polling).
- Postgres: LISTEN delivers per-notification (no coalescing), and the
forwarder's synthetic reconnect-wake covers connection gaps.
- Both: consumer code must be correct if wakes repeat, coalesce, or
arrive in any order, with at-least-once wake delivery. Exactly-once
*processing* semantics belong to queues/streams (below), never to
the wake layer.
arrive in any order. **Wakes are best-effort hints, not a per-commit
delivery guarantee** — SQLite coalescing and Postgres's no-replay hole
(a commit during a connection gap is never re-delivered) both mean
individual changes can get *no* wake; recovery is consumers' re-read,
the reconnect-wake, and idempotence — never a promised per-commit
deliver. Exactly-once *processing* semantics belong to queues/streams
(below), never to the wake layer.
Measured ground: wake latency p50 ≈ 1.1–2.2 ms on both engines at
default cadence; per-payload burst delivery verified (30/30 on pg,
@@ -49,6 +49,11 @@ implement identically):
`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)).
*(Depth annotation 2026-10-05: the key's semantics, the `StreamEvent`
shape, the ordering guarantee row, and log retention are OQ-12's —
the method *names* are pinned here, their depth was left
un-pinned, mirrored in [core-contract.md](../core-contract.md)
streams.)*
- 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,
@@ -58,7 +63,12 @@ implement identically):
- locks — `try_lock(name, owner, ttl) -> Option<Lock>` with `renew`
and `release`.
- outbox helper — `outbox(name)` with `enqueue` + `run_once` delivery
worker.
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.)*
- the tx seam — `begin_tx` / commit / rollback and the `*_tx` methods
([ADR-007](007-transactional-seam.md)).
- `subscribe(consumer) -> Box<dyn EventReceiver>` — the durable
@@ -94,6 +94,20 @@ no-renewal/expire-sweep model is not inherited):
claimable by ordinary claim (no transition fires on lapse); there
is no separate reaper for expired claims, only the budget rules
below and the no-stranded-rows sweep (§5).
- **Job-handle op validity predicate** (the D-12 disposition, stated
uniformly): every handle op (`ack`/`heartbeat`/`retry`/`fail`)
succeeds only when the row is in `processing` **and** the caller's
claim deadline has not lapsed — ADR-008 §5's false-case list
("expired, acked elsewhere, cancelled") is the same rule one
predicate-wide: deadline lapse refuses *all* ops, not just
heartbeat. The dual-execution window stays honest under this: the
original worker between lapse and reclaim may still *complete its
work* (the side effects happen), but its ack will not land and the
row is reprocessed at reclaim — at-least-once, as documented. The
reclaim wins atomically when it races the ack (one statement, same
predicate). This state check is engine-pinned in both engines'
implementations (upstream's missing-check defect class D-12 is
*not* inherited).
### 3. Retry and backoff: explicit delay or the queue's curve
+5
View File
@@ -118,6 +118,11 @@ questions affecting this document:
- **OQ-08**: capability surface (shared with
[deployment.md](deployment.md)) (open)
- **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))
Resolved: **OQ-09** (scheduler collapse —
[ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue
+4
View File
@@ -135,6 +135,10 @@ questions affecting this document:
- **OQ-06**: honker-core quality read — fork-trigger assessment —
**resolved** (2026-10-05,
[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))
Resolved: **OQ-09** (scheduler collapse —
[ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue
+115 -5
View File
@@ -26,8 +26,13 @@ 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), and
OQ-11 (fork follow-through items — substrate-side, non-consumer-facing).
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).
Resolved questions stay listed with their resolution; they are not
deleted.
@@ -297,9 +302,114 @@ 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-12: Streams depth — key semantics, `StreamEvent` shape, ordering row, retention
- **Origin**: [core-contract.md](core-contract.md) streams section,
[ADR-008](decisions/008-contract-v1-pinning.md) §1 (Phase 1
architecture review, 2026-10-05)
- **Status**: open
- **Priority**: high (streams is a first-class
[ADR-002](decisions/002-feature-scope.md) mechanism; ADR-008's
"no placeholder semantics remain" positive consequence is false for
`publish_with_key` until this resolves — the contract suite also
cannot pin cross-engine stream equivalence without an ordering row)
- **Resolution**: open. The method *names* are pinned (ADR-008 §1);
the depth is not. Sub-questions, with the evidence already gathered:
- **Key semantics** — honker's realization is a stored `key` column
(`_honker_stream(topic, key, payload)`, NULLable) with no
per-key read path or ordering enforcement anywhere; the honker-rs
doc comment says "used for per-key ordering downstream"
(`packages/honker-rs/src/lib.rs:894`), i.e. per-key global-FIFO
is an *emergent* property (a consumer reading `offset ASC`
filtered by key), not server-enforced. Options: (a) pin exactly
honker's shape — key is carried metadata, ordering stays global
FIFO (document the emergent per-key pattern), cheapest and
honest; (b) pin *server-enforced* per-key ordering (a read or
delivery guarantee keyed on `key`) — real machinery on both
engines, needs a consumer row naming the need; (c) cut
`publish_with_key` from v1 (no consumer row names keys — the
honker-rs doc comment is upstream's intent, not ours). Decision
rule: consumer-inventory row first if (b).
- **`StreamEvent` shape** — honker's: `{ offset: i64, topic: String,
key: Option<String>, payload, created_at: i64 }` with
`payload_as<T>` (upstream lib.rs:1008). The contract needs its own
pinned field list (stream name vs topic token, key in or out per
the key decision, timestamp semantics) — probably inherited
near-verbatim.
- **Ordering guarantee row** — ADR-006's table has no streams
*ordering* cell content beyond "readable"; pin FIFO-by-offset as
the read ordering (`ORDER BY offset ASC` is already both
engines' claim path) and whether any per-key strengthening rides
the key decision.
- **Retention / bounded growth** — the reference read flags
`_honker_stream` as unbounded-growth unless the user sweeps
(honker-machinery §8.5); no consumer row names stream retention
(replay-forever is the *purpose* for the named need —
subscription surfaces). Options: consumer-side trim API
(`trim_to(offset)` — a new contract method), consumer-recipe
only (new stream + advance consumers + unschedule/nothing —
needs a deletion path that doesn't exist yet, so this option is
really "document the gap"), or document nothing (honest but
repeats the notifications-table mistake ADR-010 §6 just fixed).
Rides the no-ambient-sweeper posture either way.
- **Cross-references**: OQ-04 (the pinning that left this depth
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
- **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
- **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
(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
(capability-surface shape, versioning discipline, fork-scaffold
follow-through) with its evidence base complete — no external arrivals
are being waited on.
(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.
+16 -5
View File
@@ -65,8 +65,15 @@ made under; the ADRs carry the WHY.
┌────────┐ fail(err) / budget │
│ dead │ ◄────── exhausted ───────────┘
└────────┘
│ ack deletes the row
│ (from processing, claim still valid)
│ ack deletes the row
│ (from processing, deadline unexpired)
Handle-op validity (ADR-010 §2, uniform): ack/heartbeat/retry/
fail succeed only while the row is processing and the claim's
deadline is unexpired — lapse refuses all handle ops; a worker
whose deadline lapsed before reclaim may still complete its
work, but its ack does not land (the job reprocesses at
reclaim — at-least-once).
│
▼
(gone)
@@ -84,9 +91,13 @@ made under; the ADRs carry the WHY.
[ADR-010](decisions/010-queue-semantics-depth.md) §3a).
`heartbeat(extend)` is renewal — an absolute reset from now (the new
full deadline, not additive); a **late heartbeat is refused** (never
steals the job back from a reclaimer). Between deadline lapse and
another worker's reclaim, the original worker may still complete —
the dual-execution window is the contract's honest at-least-once
steals the job back from a reclaimer). Deadline lapse refuses all
handle ops uniformly (the validity predicate,
[ADR-010](decisions/010-queue-semantics-depth.md) §2). Between
deadline lapse and another worker's reclaim, the original worker may
still complete its *work* — but its ack refuses and the row
reprocesses at reclaim — the dual-execution window is the
contract's honest at-least-once
posture; downstream idempotence is the consumer's job.
- **Reclaim consumes an attempt**: a handler that forgets to
heartbeat looks like a repeatedly-failing job and dead-letters on