Skip to content

feat(approvals): SLA auto-escalation — scheduler pass executing escalation.action #1742

Description

@os-zhuang

Context

#1740 shipped SLA display: sla_due_at (= created_at + escalation.timeoutHours) renders as countdown/overdue chips in the Console inbox. The contract side of escalation has been fully specified for a while — ApprovalEscalationSchema (packages/spec/src/automation/approval.zod.ts) carries timeoutHours, action: reassign | auto_approve | auto_reject | notify, escalateTo, notifySubmitter — but nothing executes it. Every approval action today is request-driven; there is no clock that wakes up when a pending request breaches its SLA.

Proposal

A periodic scan (via plugin-trigger-schedule integration or a dedicated interval job owned by plugin-approvals):

every N minutes:
find sys_approval_request where status='pending'
parse node_config_json.escalation; skip rows without one
overdue? execute action:
notify → messaging.emit to pending approvers + escalateTo
reassign → ApprovalService.reassign() with escalateTo as the new approver
auto_approve → decide('approve', system actor)
auto_reject → decide('reject', system actor)
audit an 'escalate' action (enum value already exists)

The execution primitives (reassign(), decide(), notify(), the escalate audit kind) all exist after #1740 — this issue is the scheduler pass plus the policy decisions below.

Decisions an ADR must settle first

  1. Idempotency / multi-instance safety — a crashed-and-restarted scanner or two concurrently running schedulers must not escalate the same request twice (double reassign; auto_approve racing auto_reject). Likely needs an escalated_at watermark on the request row (schema migration) or an idempotency lock keyed on the audit table.
  2. Semantics of a system decisionauto_approve means no human approved. Decide the audit actor convention (system:sla?), notification copy, and whether compliance-sensitive deployments should be able to forbid auto_approve/auto_reject globally.
  3. Clock ownership — does the SLA run from created_at, or re-arm after a request_info round-trip? Should the clock pause while waiting on the submitter (ServiceNow-style SLA pause)? This decides the schema.

Mis-deciding any of these is a "the machine approved 200 requests over the weekend" incident — hence ADR-level review before implementation.

References

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions