Skip to content

Make idempotency/outbox-drain an explicit driver capability; refuse loudly on drivers without it (#228, #229) - #256

Merged
choiyounggi merged 9 commits into
mainfrom
orch/p0-idempotency-1011
Oct 15, 2026
Merged

choiyounggi merged 9 commits into
mainfrom
orch/p0-idempotency-1011

Conversation

@choiyounggi

Copy link
Copy Markdown
Owner

Refs #228, #229. Tracking: #255 (P0 group).

What this does

The only production DB driver (lnpl-postgres) implements none of the optional RepositoryDriver methods for idempotency claims (idempotency_begin/finish/release) or outbox drain (drain_outbox/ack_outbox). Before this PR the core selected those features by hasattr, so on Postgres an Idempotency-Key was silently ignored (#228) and lnpl outbox drain / lnpl relay died with an AttributeError (#229). This PR makes the capability explicit and makes both consumers fail loudly.

commit change
fc74d99 RepositoryDriver.supports_idempotency() / supports_outbox_drain() — introspection default (True iff every method of the set is callable; Sqlite True, external drivers lacking the methods False, no base-class stubs). RepositoryDriverTCK gains 6 declared-unsupported cases (no skipTest; a driver that reports False must have the methods absent). docs/backends.md capability table.
c251bd3, 192c0fc lnpl outbox drain / outbox ack / relay: check the predicate before any drain/ack/open_publisher call; False → one stderr line error: backend <scheme> does not implement the outbox drain contract (drain_outbox/ack_outbox) — see docs/backends.md §3, driver closed, rc 2, empty stdout. --backend fake keeps its existing rc 2 refusal. cli-surface.md updated.
6430ba3 lnpl serve / build_app(): a RepositoryDriver whose predicate is False → requests carrying Idempotency-Key (M22) and every POST /-/events/<slug> delivery (E8) are refused with 501 idempotency-unsupported (problem+json), the workflow does not run; requests without the header run as before. One boot line on stderr (lnpl serve: repository driver <module.qualname> -- idempotency: unsupported …), GET /-/readyz 200 body carries "capabilities": {"idempotency": <bool>} (never a 503). Non-RepositoryDriver objects (test doubles) take an isolated legacy branch that preserves the previous behaviour. lexer.RESERVED_PROBLEM_CODES entry; docs/serving.md rows.
08196ac CHANGELOG [Unreleased] entries; READMEs re-pinned to the measured suite size.

Decisions recorded during the run

Verification

  • Isolated worktree at 08196ac, own venv: Ran 5394 tests in 174.778s / OK (skipped=1) (mode B included); rfc_lint, gen_plugin_references --check, check_doc_snippets, check_version_sync, ruff check impl/lnpl scripts all rc 0. Doc-cell fix 192c0fc re-checked: gen_plugin_references --check rc 0, test_plugin_references/test_readme_currency/test_cli_relay Ran 106 tests … OK.
  • Per task: independent task-reviewer approve ×4 (no blocking findings), test-quality-auditor cross-call PASS ×3 (mutation probes: predicate return True/any, old hasattr selection, guard removal — all caught), integration-reviewer approve.

🤖 Generated with Claude Code

https://claude.ai/code/session_015TZZN39cJVjPsK2Ha3C5LD

choiyounggi and others added 9 commits October 11, 2026 20:10
RepositoryDriver.supports_idempotency() / supports_outbox_drain() signatures
fixed by the t1 plan; t1 implements them in place (refs #228, #229).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TZZN39cJVjPsK2Ha3C5LD
…tes (refs #228, #229)

RepositoryDriver.supports_idempotency() / supports_outbox_drain() replace
the contract stub. Default is introspection: True only when every method
of the set (IDEMPOTENCY_METHODS / OUTBOX_DRAIN_METHODS) is callable on the
instance, so SqliteRepositoryDriver and any external driver implementing
the set report True without overriding; a missing or partial set is False.
No base-class definition is added for the five optional methods.

RepositoryDriverTCK gains 3 idempotency and 3 drain/ack cases with
declared-unsupported semantics (no skipTest): a False predicate must come
with none of the set's methods present. Reverse controls in
test_driver_contract.py prove each case fails a driver broken for it; the
SPI demo driver passes all six as declared-unsupported.

docs/backends.md §8 maps serve/CLI features to the optional methods and
the predicate that announces them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNwYixWBqEu1AM7hBArND1
… contract (refs #229)

`lnpl outbox drain`, `lnpl outbox ack` and `lnpl relay` now check
RepositoryDriver.supports_outbox_drain() right after the `fake` branch.
A driver lacking drain_outbox/ack_outbox (e.g. lnpl-postgres) gets one
stderr line naming the scheme, is closed, and the command exits 2 —
before any drain, ack, or publisher open — instead of an AttributeError
traceback. sqlite and `--backend fake` behave as before; DriverError
still maps to rc 1.

cli-surface.md documents the precondition in the outbox and relay
sections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VV85S9kXrpZ98cjrodSvwB
…otency (refs #228)

lnpl serve / build_app() used to gate the idempotency claim on
hasattr(repository, "idempotency_begin") and silently ran a keyed request
when the driver lacked it, so a retried POST executed twice. Both claim
sites now ask RepositoryDriver.supports_idempotency() (t1) through
_idempotency_mode:

- keyed workflow request, or any POST /-/events/<slug>, on a driver whose
  predicate is False -> 501 idempotency-unsupported (problem+json,
  correlation_id), nothing claimed, written or run (M22 / E8)
- unkeyed requests and --backend fake behave exactly as before; non-driver
  test doubles keep the pre-#228 opt-in (coordinator ruling t2 D1 r2)
- boot: one stderr line naming the driver when the predicate is False
- /-/readyz 200 carries {"capabilities": {"idempotency": <bool>}}; the
  closed list of five checks and the 503 contract are unchanged

docs/serving.md documents the per-backend rule, the readyz field and the
boot line; lexer.RESERVED_PROBLEM_CODES gains the new code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFLTNeouGmzkiMoo9NcXWg
CHANGELOG.md Unreleased gains the capability predicates + TCK entry
(Added) and the serve 501 idempotency-unsupported and outbox CLI rc 2
refusal entries (Fixed). README.md / README.ko.md now state the measured
full-suite count: Ran 5394 tests in 154.320s (prose ~5,390).

Refs #228, #229

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjcjDfZcNtdLQiFjJnQJLY
…rain contract (refs #229)

The relay flag table still said the backend was sqlite-only, which
contradicted the outbox drain/ack cells and the relay precondition
paragraph. It now names sqlite or any registered driver implementing
drain_outbox and ack_outbox, with everything else (and `fake`) refused
with rc 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VV85S9kXrpZ98cjrodSvwB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant