Skip to content

fix(kernel): record scoped-admission effect failures once, not twice (#244) - #255

Merged
George-RD merged 4 commits into
mainfrom
George-RD/dev-244
Aug 21, 2026
Merged

fix(kernel): record scoped-admission effect failures once, not twice (#244)#255
George-RD merged 4 commits into
mainfrom
George-RD/dev-244

Conversation

@George-RD

@George-RD George-RD commented Aug 21, 2026

Copy link
Copy Markdown
Owner

What

Record a failed scoped-admission effect exactly once instead of twice, and make
the record-once guarantee hold even when the fail-closed owner-digest write
fails after the primary record is committed.

Why

From the Effect Truth architecture-review checkpoint (#244). With the settlement
seams merged (EffectDisposition + settle_reservations #238, begin_effect/
settle_effect #248), the scoped executor owns the durable record for every
disposition it returns. The mediation error handler in
mediate_and_dispatch_action (api/actions.rs) was still re-appending
action.dispatch_failed and re-calling batch_failure on top of that record,
so one failed scoped draft produced two owner-digest rows and two audit rows,
and pre-effect NotAttempted refusals (target/payload mutation) were mis-filed
under the Connector-class vocabulary.

How

  1. Record-once at the mediation seam. Thread a typed executor_recorded: bool on DispatchedEffect. Scoped Ok(..) arms set it true (the executor
    already audited/batched); the generic connector path, the scoped registry
    miss (NoExecutor), and pre-record executor errors set it false. The
    mediation error handler returns early when the executor owns the record, so
    each failure yields exactly one digest row + one audit row, with parity
    between the scoped and interactive-approval paths.

  2. Fail-closed provenance. In create_approved_draft the primary failure
    record (settle_effect(..ConfirmedFailure..) / append_audit) commits
    before the secondary, deliberately fail-closed batch_failure. A batch-store
    error after that point previously returned a bare Err, which mapped to
    executor_recorded: false and made the handler re-audit — a double-record on
    a genuine failure path. Added RecordedEffectError { disposition, source }:
    the four post-record batch_failure arms now route through
    batch_then_record, returning the settled disposition on success or a
    RecordedEffectError on a fail-closed digest error — never a bare Err
    (which double-records) and never a swallowed success (which would drop the
    digest with no redrive). The executor-result -> DispatchedEffect mapping is
    extracted into a pure dispatched_from_executor that downcasts it to
    executor_recorded: true with the settled disposition while still surfacing
    the error for redrive.

Tests

  • scoped_admission_outcome_tests: failure_after_attempt_cancels_reservation
    and pre_effect_refusal_cancels_reservation_without_writing assert
    exactly-once counts (1 executor audit, 0 action.dispatch_failed, correct
    digest-row count).
  • scoped_admission_parity_tests:
    • the scoped and interactive draft paths produce an identical single-record
      footprint;
    • recorded_effect_error_maps_to_executor_owned_surfaced_failure and
      plain_executor_error_stays_unrecorded_and_fails_closed prove the pure
      provenance mapping;
    • digest_write_failure_after_settle_surfaces_recorded_effect_error proves
      the fail-closed digest path (1 draft.creation_failed audit, 0 digest rows,
      error surfaced) using the artifact store's existing set_fault_put_for_test
      seam.
  • ./scripts/check.sh passes (45 spec checks + full cargo suite); rebased on
    origin/main.

Cross-merge fixup (refs #251/#252)

Rebasing onto origin/main surfaced a semantic merge conflict independent of
#244: #251 added the required BriefcaseSection.origin field and updated
existing literals, but #252 landed a private_section() test literal without
it, breaking cargo test compilation. Added the missing origin: None,
(matching the sibling literal in the same file) as a separate commit. Coordinator
approved.

Implementation notes (five-line summary)

  1. Bug: mediation error handler re-audited action.dispatch_failed and re-called batch_failure for scoped effects the executor had already recorded, double-counting failures and mis-classing pre-effect refusals as Connector-class.
  2. Fix: typed executor_recorded flag on DispatchedEffect; the handler returns early when the executor owns the record. Ok(..) arms set it true; generic path, registry-miss, and pre-record errors set it false.
  3. Closed the fail-closed edge: a post-record digest-store error now surfaces as RecordedEffectError (keeps the failure visible/redrivable) while preserving record-once provenance, instead of a bare Err that double-records or a swallowed success that drops the digest.
  4. Tests: two outcome tests assert exactly-once counts; a parity test proves the scoped and interactive paths share one record shape; three new tests prove the provenance mapping and the fail-closed digest path (via the existing set_fault_put_for_test seam).
  5. Gate: ./scripts/check.sh passes (45 spec checks + full cargo suite); rebased on origin/main.

Closes #244


Summary by cubic

Record scoped-admission failures exactly once and preserve provenance when the post-record owner-digest write fails. Previously, the mediator re-appended action.dispatch_failed and re-batched after executors already recorded, causing duplicate digest/audit rows and misclassifying pre-effect refusals.

  • Introduces executor_recorded on DispatchedEffect and makes the mediator return early on executor-owned records, preventing duplicate audit/digest writes.
  • Adds RecordedEffectError and routes post-record digest writes through batch_then_record so fail-closed digest errors surface without triggering re-recording; Ok dispositions and RecordedEffectError map to executor_recorded=true via a pure dispatched_from_executor.
  • Keeps unrecorded executor errors (pre-record failures) as DeliveryUnknown with executor_recorded=false so mediation records them once as Resource.
  • Sets executor_recorded=false on the generic connector path and on scoped registry-miss.
  • Behavior change: one failed scoped draft now yields exactly one executor-typed audit and one digest row; pre-effect NotAttempted refusals audit once under executor vocabulary and do not create Connector-class digest entries or action.dispatch_failed audits.
  • Tests: adds parity and provenance tests proving exactly-once recording and fail-closed digest handling; fixes a cross-merge test literal by adding missing BriefcaseSection.origin.

Written for commit 29f09fa. Summary will update on new commits.

Review in cubic

…244)

The mediation error handler in mediate_and_dispatch_action re-appended
action.dispatch_failed and re-called batch_failure for scoped effects
whose executor already self-audited and self-batched its typed
disposition. This double-counted a single failed scoped draft (two
digest rows, two audit rows) and mis-filed pre-effect NotAttempted
refusals (target/payload mutation) under the Connector-class vocabulary.

Thread a typed executor_recorded flag on DispatchedEffect: the scoped
executor owns the record for every disposition it returns, the generic
connector path and the un-recorded scoped arms (registry miss, executor
error) leave it false. The mediation handler returns early when the
executor already owns the record, so each failure is recorded exactly
once with parity between the scoped and interactive-approval paths.

Closes #244
…est write fails (#244)

The scoped executor commits its typed failure record (settle_effect /
append_audit draft.creation_failed) BEFORE the secondary, fail-closed
owner-digest batch_failure. A batch-store error after that point returned
a bare Err, which dispatch_scoped_effect mapped to executor_recorded=false
-> the mediation handler re-audited action.dispatch_failed: a double-record
on a genuine failure path.

Add RecordedEffectError { disposition, source }: the four post-record
batch_failure arms route through batch_then_record, returning the settled
disposition on success or a RecordedEffectError on a fail-closed digest
error (never a bare Err, never a swallowed success that would drop the
digest with no redrive). Extract the executor-result -> DispatchedEffect
mapping into a pure dispatched_from_executor that downcasts it to
executor_recorded=true with the settled disposition while still surfacing
the error for redrive. Prove it with a pure mapping test and a fail-closed
digest test using the artifact store's existing set_fault_put_for_test seam.
…iteral (refs #251/#252)

Cross-merge fixup: #251 added the required BriefcaseSection.origin field and
updated existing literals; #252 landed a private_section() test literal
without it. Their combination on main broke cargo test compilation. Add the
missing 'origin: None,' matching the sibling literal in the same file.

Coordinator-approved cross-merge fixup unblocking the #244 gate.
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@George-RD, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2abb585f-7f21-4c0e-811c-6b770667474b

📥 Commits

Reviewing files that changed from the base of the PR and between ac30339 and 29f09fa.

📒 Files selected for processing (9)
  • crates/openspine-kernel/src/api/actions.rs
  • crates/openspine-kernel/src/api/connector_breaker.rs
  • crates/openspine-kernel/src/api/effect_executors.rs
  • crates/openspine-kernel/src/api/scoped_admission.rs
  • crates/openspine-kernel/src/api/scoped_admission_outcome_tests.rs
  • crates/openspine-kernel/src/api/scoped_admission_parity_tests.rs
  • crates/openspine-kernel/src/api/scoped_admission_support.rs
  • crates/openspine-kernel/src/disclosure/disclosure_messaging_tests.rs
  • crates/openspine-kernel/src/pipeline/approval_draft.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@George-RD
George-RD merged commit 4e2a316 into main Aug 21, 2026
3 checks passed
@George-RD
George-RD deleted the George-RD/dev-244 branch August 21, 2026 07:36
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.

Record scoped-admission effect failures once, not twice

1 participant