ADR-071: Guard retained pending rejection with exact review
Date: 2026-10-17 Status: Accepted Author: Rômulo Febasi Amends: ADR-063 operator decision contract
Context
Soma retains interrupted knowledge intentions and needs to discard an exact reviewed proposal without publishing it. An ID-only rejection cannot attest which payload was displayed. The legacy path appends audit before checking whether the row is still pending, producing a false rejected entry when an accepted or rejected row is selected. Its status update already restricts the pending state, but does not coordinate that check and the audit with other native writers.
Decision
Add reject to the version-one local-owner knowledge descriptor. The existing
scoped and bounded request envelope carries
{"op":"reject","pending_id":42,"review_sha256":"<64 lowercase hex>"}.
Return {pending_id,status:"rejected",review_sha256,retried}. Existing descriptors
without this capability remain valid; clients retain the intention and report an
unsupported decision instead of falling back to an ID-only legacy command.
The CLI and MCP knowledge doors call the same core contract. The agent MCP door
refuses this operator action with local_operator_rejection_required, just as
acceptance requires an explicit local operator. Shared authority remains unsupported
by this local-owner contract. Legacy pending reject shares the native writer and
pending-state guard but makes no exact displayed-review claim.
Hold the existing writer lease and an immediate SQLite transaction while reading
and comparing the retained row, recording the decision and committing its state.
Compare the reviewed digest over ID, kind, payload, context, reason and source.
A changed review fails without an audit append. Only pending rows can transition;
accepted, auto-accepted and undone rows cannot be rejected. A repeated exact
knowledge rejection of an unchanged rejected row succeeds with retried:true
without another audit entry or timestamp change. Legacy repeated ID-only rejection
refuses without appending. No new request ledger or schema migration is needed;
status and decision timestamps are deliberately outside the existing review digest.
Rejection discards a retained proposal and publishes no source. It can therefore discard a stale proposal after source, accepted-head or proposal-policy changes. It does not relax acceptance checks, revalidate stale evidence for publication, change accepted seals, prune the queue or remove prior audit entries. Current local-owner workspace selection and the existing shared-authority boundary still apply. Advisory locking excludes cooperative native writers, not external editors.
Consequences
The row, exact reviewed payload and rejection audit remain available for recovery. Changed or missing retained rows cannot support an exact retry. Explicit pruning, index destruction or direct database rewriting remain outside the retained-index idempotency claim in ADR-063.
Audit and SQLite are separate durability domains. The conditional status update is staged inside SQLite before the audit append; audit failure rolls back that update. The append precedes SQLite commit. A crash or commit failure after append can leave an audit entry while the row remains pending, and a later retry can add another entry. The writer lease and transaction prevent contradictory cooperative live decisions, but do not make the audit file and SQLite physically atomic or provide crash-safe exactly-once audit delivery. Source transaction durability in ADR-056 is not extended to this operational audit file by this decision.
Alternatives considered
- Invoke legacy reject after a separate review: a race can discard different bytes.
- Require current publication preconditions: prevents discarding stale proposals even though rejection publishes nothing.
- Add a durable decision ledger: broader recovery work than this retained-index contract; revisit if crash-safe exactly-once audit delivery becomes required.
Verification
Real workspace assertion tests cover accepted-state audit preservation, exact rejection and retry, changed digest/payload/context/metadata, stale-source discard, writer contention, retained rows and unchanged source/seals. MCP smoke verification checks the operator rejection boundary and CLI review parity. Core tests exercise writer contention without relying on process startup. An independent mutation challenge and the repository quality gates accompany the implementation evidence.