Skip to content

A case action's event and its patch are two writes with no transaction — and the run path can lose the event #598

Description

@Taleef7

Two related gaps found by the 2026-09-07 review. Accepted on 2026-09-08, unchanged since, no issue until now.

1. An operator action is two writes with no transaction

backend-ts/src/case/case-actions.ts records the event (:138) and then applies the patch (:150). A failure between them leaves an action recorded but not applied.

The direction is deliberate and should stay: it can never produce an unaudited state change, which is the hard rule in CLAUDE.md. What is missing is the primitive — there is no applyCaseAction({ patch, action, audit }) that makes the three one unit.

2. The run pipeline is the other way round, and CAN lose the event

This is the part the review's framing understated and the follow-up assessment corrected. run-pipeline.ts upserts the case and then best-effort audits it, catching an audit-insert failure and logging a run WARN rather than aborting. That ordering is also deliberate — it exists so a transient audit_events failure cannot strand an otherwise-complete run as RUNNING after the case was already mutated.

But it means the system-wide claim is false as stated. Carry the narrower true statement:

"Every state change writes an audit_event" holds for operator actions. It does not hold for the run-created case transition, which can mutate and lose the event.

That belongs in DATA_MODEL_CONTRACTS §4 next to the best-effort note, rather than being discoverable only by reading the pipeline.

What was decided about the fix

A reconciliation job was proposed and qualified: it is not magic — it needs durable operation identity, an expected version, a deadline, and a visible failure state, or it is a second unreliable thing checking the first. Before any real operational reliance, the state change, the action record and the audit row should be one transaction, including the run-created case transition.

Partial progress already exists and is worth building on rather than around: recordCaseEvents is a batch atomic dual-write, added for bulk assign and reused for panel backfill. Bulk assign deliberately audits before it mutates — the same side of the trade as the single-action path.

Related: ADR-076, ADR-080, CLAUDE.md hard rules.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions