Eskalasi otomatis dari bot, sistem evaluasi machine learning, atau background worker ke antrean review manual (Human-in-the-Loop / HITL) sering kali memicu dua masalah utama: queue churn akibat payload yang tidak memiliki konteks diagnostik memadai, serta race condition saat operator mengeklaim tiket secara bersamaan. Tanpa kontrak API yang ketat, operator manusia menghabiskan waktu merekonstruksi status sistem, atau mengerjakan tiket yang sudah diambil operator lain.

1. Desain Payload: Mewajibkan Context Proof

Penyebab utama queue churn adalah pengiriman payload minimal seperti {"error": "failed", "entity_id": 102}. Operator tidak dapat mengambil keputusan tanpa riwayat eksekusi. Kontrak eskalasi harus mewajibkan context proof bundle dan precheck hash.

Precheck hash merupakan kalkulasi SHA-256 dari langkah self-healing otomatis yang gagal. Hal ini membuktikan bahwa agen atau background worker telah menjalankan prosedur validasi lokal sebelum membuang beban kerja ke manusia.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "HITLEscalationRequest",
  "type": "object",
  "required": [
    "escalation_id",
    "idempotency_key",
    "entity_type",
    "entity_id",
    "reason_code",
    "precheck_hash",
    "diagnostic_bundle"
  ],
  "properties": {
    "escalation_id": { "type": "string", "format": "uuid" },
    "idempotency_key": { "type": "string", "minLength": 16 },
    "entity_type": { "type": "string" },
    "entity_id": { "type": "string" },
    "reason_code": { "type": "string" },
    "precheck_hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
    "diagnostic_bundle": {
      "type": "object",
      "required": ["trace_id", "attempted_actions", "input_snapshot", "evaluator_verdict"],
      "properties": {
        "trace_id": { "type": "string" },
        "attempted_actions": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["action", "executed_at", "result"],
            "properties": {
              "action": { "type": "string" },
              "executed_at": { "type": "string", "format": "date-time" },
              "result": { "type": "string" }
            }
          }
        },
        "input_snapshot": { "type": "object" },
        "evaluator_verdict": {
          "type": "object",
          "required": ["confidence_score", "threshold"],
          "properties": {
            "confidence_score": { "type": "number" },
            "threshold": { "type": "number" }
          }
        }
      }
    }
  }
}

2. Idempotensi dan Pencegahan Escalation Storm

Saat terjadi kegagalan berseri, sistem otomatis cenderung mengirimkan ulang payload eskalasi pada setiap retry loop. Tanpa deduplikasi, antrean manual terdistorsi oleh duplikasi entity yang sama.

Gunakan kombinasi database unique constraint dan Redis short-lived lock:

  • Unique constraint: Gabungan (entity_type, entity_id) dibatasi hanya boleh memiliki satu record berstatus aktif (PENDING_REVIEW atau CLAIMED).
  • Idempotency key: Endpoint eskalasi menyimpan idempotency_key dengan masa kedaluwarsa (TTL) 24 jam untuk mendeteksi re-entrant payload identik.
CREATE TABLE review_tasks (
    id UUID PRIMARY KEY,
    idempotency_key VARCHAR(64) NOT NULL UNIQUE,
    entity_type VARCHAR(64) NOT NULL,
    entity_id VARCHAR(64) NOT NULL,
    status VARCHAR(32) NOT NULL DEFAULT 'PENDING_REVIEW',
    precheck_hash CHAR(64) NOT NULL,
    diagnostic_bundle JSONB NOT NULL,
    version INT NOT NULL DEFAULT 1,
    claimed_by VARCHAR(64),
    lease_token UUID,
    lease_expires_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Mencegah duplikasi tugas aktif untuk entitas yang sama
CREATE UNIQUE INDEX uq_active_entity_task 
ON review_tasks (entity_type, entity_id) 
WHERE status IN ('PENDING_REVIEW', 'CLAIMED');

3. Finite State Machine (FSM) dan State Lock

Antrean HITL melibatkan tiga status utama:

  1. PENDING_REVIEW: Tersedia dalam antrean untuk dikerjakan.
  2. CLAIMED: Sedang dievaluasi oleh operator. Terikat pada lease_token berdurasi terbatas (misal: 15 menit) untuk mencegah tiket tertahan saat operator menutup browser tanpa submit.
  3. RESOLVED: Keputusan selesai, hasil dicatat, payload diteruskan ke callback.

Mekanisme Atomic Claiming

Jangan gunakan alur SELECT lalu UPDATE terpisah. Gunakan conditional update langsung pada database untuk menjamin atomic operation tanpa distributed transaction overhead.

-- Mengklaim tiket dengan lease lock 15 menit
UPDATE review_tasks
SET 
    status = 'CLAIMED',
    claimed_by = :operator_id,
    lease_token = :generated_token,
    lease_expires_at = NOW() + INTERVAL '15 minutes',
    version = version + 1,
    updated_at = NOW()
WHERE id = :task_id 
  AND (
      status = 'PENDING_REVIEW' 
      OR (status = 'CLAIMED' AND lease_expires_at < NOW())
  );
-- ponytail: jika returning rows = 0, tiket telah diambil atau lease masih valid di operator lain.

4. Route Handler: Validasi Kontrak & State Transition

Implementasi Express/TypeScript untuk endpoint eskalasi dan klaim:

import { Request, Response, Router } from 'express';
import crypto from 'crypto';
import { db } from './db';

const router = Router();

// Endpoint Eskalasi Task
router.post('/tasks/escalate', async (req: Request, res: Response) => {
  const { escalation_id, idempotency_key, entity_type, entity_id, precheck_hash, diagnostic_bundle } = req.body;

  // 1. Validasi keberadaan context proof minimum
  if (!diagnostic_bundle?.attempted_actions?.length || !diagnostic_bundle?.evaluator_verdict) {
    return res.status(422).json({
      error_code: 'ERR_INSUFFICIENT_CONTEXT_PROOF',
      message: 'Eskalasi ditolak: diagnostic bundle wajib mencakup trace, attempted actions, dan verdict.',
      missing_fields: ['diagnostic_bundle.attempted_actions', 'diagnostic_bundle.evaluator_verdict']
    });
  }

  // 2. Validasi integritas precheck hash
  const computedHash = crypto
    .createHash('sha256')
    .update(JSON.stringify(diagnostic_bundle.attempted_actions))
    .digest('hex');

  if (computedHash !== precheck_hash) {
    return res.status(422).json({
      error_code: 'ERR_PRECHECK_HASH_MISMATCH',
      message: 'Hash diagnostik pre-check tidak valid dengan bukti aksi yang disertakan.'
    });
  }

  try {
    await db.query(
      `INSERT INTO review_tasks (id, idempotency_key, entity_type, entity_id, precheck_hash, diagnostic_bundle)
       VALUES ($1, $2, $3, $4, $5, $6)`,
      [escalation_id, idempotency_key, entity_type, entity_id, precheck_hash, JSON.stringify(diagnostic_bundle)]
    );
    return res.status(201).json({ status: 'PENDING_REVIEW', task_id: escalation_id });
  } catch (err: any) {
    if (err.code === '23505') { // Unique violation Postgres
      return res.status(409).json({
        error_code: 'ERR_TASK_CONFLICT',
        message: 'Task aktif untuk entitas ini sudah terdaftar dalam antrean.'
      });
    }
    return res.status(500).json({ error_code: 'ERR_INTERNAL_SERVER' });
  }
});

// Endpoint Klaim Task oleh Operator
router.post('/tasks/:id/claim', async (req: Request, res: Response) => {
  const { id } = req.params;
  const { operator_id } = req.body;
  const leaseToken = crypto.randomUUID();

  const result = await db.query(
    `UPDATE review_tasks
     SET status = 'CLAIMED', claimed_by = $1, lease_token = $2, lease_expires_at = NOW() + INTERVAL '15 minutes', version = version + 1, updated_at = NOW()
     WHERE id = $3 AND (status = 'PENDING_REVIEW' OR (status = 'CLAIMED' AND lease_expires_at < NOW()))
     RETURNING id, lease_token, lease_expires_at`,
    [operator_id, leaseToken, id]
  );

  if (result.rowCount === 0) {
    return res.status(409).json({
      error_code: 'ERR_STATE_CONFLICT',
      message: 'Tiket sudah diklaim oleh operator lain atau sedang dalam proses penyelesaian.'
    });
  }

  return res.status(200).json(result.rows[0]);
});

export default router;

5. Penyelesaian dan Webhook Callback Contract

Saat operator menyelesaikan review, payload keputusan dikirim melalui POST /tasks/:id/resolve. Sistem memverifikasi lease_token milik operator yang bersangkutan, mengubah status menjadi RESOLVED, lalu memicu webhook ke sistem sumber secara asynchronous.

POST /tasks/a7b1c4e2-8921-4f1b-b461-71e9c20a9a12/resolve
Content-Type: application/json
X-Lease-Token: 9e32a688-6628-4e56-91e8-636279f1dc85

{
  "operator_id": "usr_op_771",
  "decision": "REJECTED",
  "notes": "Anomali format transaksi melanggar rule AML-04.",
  "resolution_payload": {
    "override_action": "BLOCK_TRANSACTION",
    "freeze_account": false
  }
}

Respons dari webhook callback ke consumer harus menyertakan X-Signature-256 berbasis HMAC SHA-256 untuk memvalidasi bahwa payload benar-benar berasal dari sistem HITL internal, bukan injeksi eksternal.

Aturan Konkurensi: Jangan izinkan ekstensi lease tanpa verifikasi token aktif. Jika masa lease habis, operator tidak boleh mengirimkan penyelesaian sebelum melakukan pembaruan lease secara eksplisit.