Chapter 40 — Human Review and Case Management
O-1001 needs a reship email. Policy passed. Outreach drafted. Maya softens one sentence and hits modify.
The worker resumes and sends the model’s original draft anyway. Jordan gets the line Maya removed. You built a theatre desk, not human review.
The case desk is part of the product. Reviewers get a read API for the evidence packet — proposal, policy reasons, required-fields checklist, contact history — and actions to approve, reject, or modify. Every decision lands in the audit trail and patches the checkpoint so the worker sends the bytes that were actually approved.
worker parks on approval → Maya reads packet → approve | reject | modify → checkpoint + audit → worker resumes → Outreach sends approved body
Without checkpoint-backed modify, HITL is cosmetic. Without a read API, reviewers decide from model prose alone. Without reviewer_id on every decision, accountability vanishes when something goes wrong.
This chapter adds the review APIs and checkpoint patch that bind human authority to what Outreach actually sends.
First principles
- Review UX is a first-class API, not a SQL console.
- Approve / reject / modify are distinct transitions with audit events.
- Modify updates the checkpointed draft before send.
- Reviewers see evidence, policy reasons, and diffs — not only model prose.
- Accountability: reviewer_id on every decision (Ch 29–30).
- Safe defaults remain on timeout.
Concrete example
Reviewer opens O-1001. The packet shows the stalled-shipment exception proposal, contact history, draft order update, evidence links, and policy version shop_v1.0.0. They soften one sentence and approve as modify.
1. GET /cases/O-1001/review → EvidencePacket + draft + policy_reasons
2. POST /approvals/appr-77/decide
{ "approve": true, "modified_body": "…edited…", "reviewer_id": "rev-17" }
3. Checkpoint.state.draft_body = edited
4. Audit: approval status=modified + body hash
5. Worker resumes → Outreach.send uses edited body
Diagram
stateDiagram-v2
[*] --> AwaitingApproval
AwaitingApproval --> Approved: approve
AwaitingApproval --> Modified: modify
AwaitingApproval --> Rejected: reject
AwaitingApproval --> Timeout: timer
Approved --> Sending: worker resume
Modified --> Sending: worker resume with new body
Rejected --> SafeStop
Timeout --> SafeStop
Review UI ──► read API (packet)
◄── draft, reasons, evidence ids, history
Review UI ──► decide API
──► audit + checkpoint patch + queue signal
Worker ──► load checkpoint ──► send approved bytes
Caption: Notice the worker never trusts the original model draft after a modify — checkpoint is source of truth.
Implementation — read/decide + checkpoint patch
Builds on the ApprovalQueue from Ch 29 and AuditLog from Ch 30.
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from shopops.audit import AuditLog, hash_payload
@dataclass
class CaseCheckpoint:
case_id: str
tenant_id: str
fsm: str
draft_body: str | None = None
approval_id: str | None = None
version: int = 1
@dataclass
class ReviewAPI:
"""Teaching stand-in for the case desk backend."""
queue: Any # ApprovalQueue from Ch 29
checkpoints: dict[str, CaseCheckpoint] = field(default_factory=dict)
audit: AuditLog = field(default_factory=AuditLog)
def get_review(self, case_id: str) -> dict[str, Any]:
cp = self.checkpoints[case_id]
req = self.queue._items[cp.approval_id]
return {
"case_id": case_id,
"fsm": cp.fsm,
"draft_body": cp.draft_body,
"packet": {
"summary": req.packet.summary,
"policy_reasons": list(req.packet.policy_reasons),
"evidence_ids": list(req.packet.evidence_ids),
"action": req.packet.action,
},
"approval_id": req.id,
"status": req.status.value,
}
def decide(
self,
approval_id: str,
*,
reviewer_id: str,
approve: bool,
modified_body: str | None = None,
note: str = "",
) -> dict[str, Any]:
req = self.queue.decide(
approval_id,
reviewer_id=reviewer_id,
approve=approve,
note=note,
modified_body=modified_body,
)
case_id = req.packet.case_id
cp = self.checkpoints[case_id]
if req.status.value == "modified":
cp.draft_body = req.modified_body
cp.version += 1
cp.fsm = "approved_to_send"
elif req.status.value == "approved":
cp.fsm = "approved_to_send"
else:
cp.fsm = "failed_safe"
self.audit.append(
case_id=case_id,
tenant_id=cp.tenant_id,
kind="approval",
actor=f"human:{reviewer_id}",
payload={
"approval_id": approval_id,
"status": req.status.value,
"note": note,
"body_hash": hash_payload(cp.draft_body or ""),
"checkpoint_version": cp.version,
},
)
return {"case_id": case_id, "fsm": cp.fsm, "draft_body": cp.draft_body}
# Worker resume sketch
def resume_send(cp: CaseCheckpoint, outreach_send) -> None:
assert cp.fsm == "approved_to_send"
assert cp.draft_body is not None
outreach_send(body=cp.draft_body) # not the stale model copy
Failure modes
| Failure | Effect | Fix |
|---|---|---|
| UI shows packet; worker uses old draft | Wrong message sent | Checkpoint patch on modify |
| Approve without reading evidence | Rubber stamp risk | UX forces checklist; sample audits |
| No audit on modify | Reconstruction fails | Mandatory approval events |
| Race: two reviewers | Conflicting decisions | Single approval_id + CAS version |
| Timeout then late approve | Surprising send | Reject late decides; new ticket |
Production considerations
- Keyboard-first case desk; show policy denies chronologically.
- Metrics: modify rate, time-to-decide, disagreement with model drafts.
- RBAC: reviewers ≠ engineers with prod DB.
- Store reviewer-visible redaction views vs sealed full PII packs.
- Load simulation: burst approvals after an outage — queue depth SLOs.
Chapter summary
- Case desk APIs make HITL real.
- Approve / reject / modify are audited transitions.
- Modify patches checkpointed draft bodies.
- Workers send approved bytes only.
- Evidence packets beat chat screenshots.
- Timeouts stay safe; late approves need new tickets.
- Reviewer identity is mandatory.
- The desk is product surface, not an afterthought.
Exercises
- Mechanical. Modify a draft via
ReviewAPI.decide; assert checkpointdraft_bodymatches and audit status ismodified. - Race. Two
decidecalls on one approval_id; second must fail; fsm unchanged after first reject. - Design. Wireframe the reviewer screen: which five fields are non-negotiable above the fold?
References
- Chapters 8, 29–30, 39
- Human factors / approval workflow patterns. [VERIFY]
- SR 11-7 — documentation and control challenge processes (governance framing). 2011.