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
- 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. - Semantics of a system decision —
auto_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. - 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
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) carriestimeoutHours,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-scheduleintegration or a dedicated interval job owned by plugin-approvals):The execution primitives (
reassign(),decide(),notify(), theescalateaudit kind) all exist after #1740 — this issue is the scheduler pass plus the policy decisions below.Decisions an ADR must settle first
escalated_atwatermark on the request row (schema migration) or an idempotency lock keyed on the audit table.auto_approvemeans 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.created_at, or re-arm after arequest_inforound-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
sla_due_at), Console chips: feat(console): approvals thread UI — reassign, remind, request-info, reply, SLA chips, step progress objectui#1653ApprovalEscalationSchemainpackages/spec/src/automation/approval.zod.ts🤖 Generated with Claude Code