KB-7002

Phase 2 — DLQ Governance (triage + dry-run requeue)

5 min read Revision 1
dieu45phase2dlq-governancetriage2026-05-26

DLQ Governance — triage update + dry-run requeue preview

Two-function shape (Phase 2 ships triage + dry-run only)

Function Mode Mutates Gated by
fn_job_dead_letter_triage_update(p_dead_letter_id, p_triage_status, p_actor, p_triage_note) VOLATILE, SECURITY DEFINER Yes (DLQ row only — never job_queue) — (vocab CHECK is enforcement)
fn_job_dead_letter_requeue_dry_run(p_dead_letter_id) STABLE No — (read-only)

No apply-side requeue function ships in Phase 2. queue.dlq.replay_enabled is added as false so any future requeue mutator inherits a gate-off-by-default.

Triage vocab

Pinned to the existing job_dead_letter_triage_check CHECK constraint:

ARRAY['pending', 'acknowledged', 'manual_replay', 'escalated', 'closed']

fn_job_dead_letter_triage_update validates against this vocab in Postgres (pre-CHECK) so refusals carry a structured response rather than a raw constraint violation.

Triage update — happy path

Proof step 6:

SELECT fn_job_dead_letter_triage_update(
    p_dead_letter_id => '9a4f319f-3220-4c8e-8a11-a9bfb46b2796',
    p_triage_status  => 'acknowledged',
    p_actor          => 'phase2_proof_triage',
    p_triage_note    => 'phase2 bounded proof triage'
);

{
  "refused": false,
  "triaged_at": "2026-05-26T13:54:54.639694+00:00",
  "triaged_by": "phase2_proof_triage",
  "dead_letter_id": "9a4f319f-3220-4c8e-8a11-a9bfb46b2796",
  "triage_status_to": "acknowledged",
  "triage_status_from": "pending"
}

Updates only triage_status, triage_note (preserves if NULL), triaged_at, triaged_by. Never mutates job_queue or any other row.

Triage update — refusals (verified in proof step 11)

Test Response
invalid status 'bogus_status' {"got":"bogus_status","reason":"invalid_triage_status","allowed":["pending","acknowledged","manual_replay","escalated","closed"],"refused":true}
empty actor '' {"reason":"actor_required","refused":true}
unknown dead_letter_id {"reason":"dead_letter_not_found","refused":true,"dead_letter_id":"…"}

Dry-run requeue — preview shape

fn_job_dead_letter_requeue_dry_run(p_dead_letter_id) returns:

{
  "mutation": false,
  "evaluated_at": "<now>",
  "replay_gate_enabled": false,
  "dead_letter_id": "<uuid>",
  "job_id_original": "<uuid>",
  "job_kind": "<text>",
  "triage_status": "<text>",
  "final_error": "<text>",
  "attempts_at_failure": <int>,
  "idempotency_key": "<text>",
  "idempotency_collision": {"blocked": <bool>, "active_job_id": "<uuid>"?},
  "would_action": "refused: queue.dlq.replay_enabled=false" |
                  "refused: idempotency_collision" |
                  "preview_only: Phase 2 ships no apply-side requeue"
}

would_action is the operator-readable verdict:

  1. If queue.dlq.replay_enabled=false → first refusal wins.
  2. Else if there is an active job_queue row with the same idempotency_key → collision refusal.
  3. Else → preview_only.

Proof outputs (step 7 + step 11f)

After step 6, the DLQ row's triage_status was acknowledged. Step 7 ran:

{
  "mutation": false,
  "replay_gate_enabled": false,
  "triage_status": "acknowledged",
  "would_action": "refused: queue.dlq.replay_enabled=false",
  "idempotency_collision": {"blocked": false},
  "attempts_at_failure": 5,
  "final_error": "lease_expired_reaped_at_max_attempts",
  …
}

Step 11f (unknown UUID):

{"reason":"dead_letter_not_found","refused":true,
 "dead_letter_id":"00000000-0000-0000-0000-000000000000"}

Why no apply-side requeue in Phase 2

Two reasons:

  1. Phase 2 scope is governance, not workflow. Requeue mutates job_queue which is governed by queue.job_substrate.enabled (Phase 3+).
  2. Replay design needs idempotency-key handling (re-use vs. new key, attempts reset semantics) — DP6 still has open questions in the design pack.

DLQ retention (out of Phase 2)

DP7 retention policy (365d for DLQ) is design-only; no pruning function ships. Operators currently keep DLQ rows indefinitely.

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-dieu45-phase-2-heartbeat-activation-lease-governance/04-dlq-governance.md