Skip to content

HITL 核准流程的錯誤路徑 lifecycle 處理 #128

@quan0715

Description

@quan0715

背景

HITL(Human-in-the-Loop)寫入核准流程已串通(PR #51、commit `8203ac03`),但錯誤路徑的 lifecycle 有幾處破口:當 `POST /api/v1/ai/runs/{id}/approval/` 或後續 Celery dispatch 失敗時,run 可能卡在前後端無法 recover 的狀態。

呼叫路徑

`submitApproval` → `resumeAgent` → `chatbotRepository.submitRunApproval()` → backend `POST /approval/` → `resume_approval_run`:

```python

backend/apps/ai/services/run_runtime.py:136-150

run.kind = RESUME
run.resume_decision = decision
run.status = RUNNING
run.save(...) # ① DB 更新
dispatch_run(run) # ② Celery enqueue
```

四種錯誤情境的當前行為

情境 前端行為 run 狀態 後果
A. DB 斷 / backend crash(在 ① 前 500) HITLCard 消失、Toast error 留在 `AWAITING_APPROVAL` 使用者沒 UI 可重試:`pendingApproval` 已被 `submitApproval` 清成 `null`,只能 refresh 或 cancel
B. dispatch ② 失敗(Celery/Redis down) 同上 已 `RUNNING` 但沒 task 殭屍 run,永遠沒進度
C. Celery 成功,ai-service fail / 連線斷 下次 SSE 訂閱收到 `run_failed` → `onError` + 移除 activeRun `FAILED` ✓ 正常
D. 使用者按 cancel `cancelRun` 200、activeRun 移除 `CANCELLED` ✓ 正常(`request_run_cancel` 處理了 awaiting 狀態)

主要破口

  1. `pendingApproval` 過早清空 — `useChatbot.ts:624` 在呼叫 `resumeAgent` 前就 `setPendingApproval(null)`,approval request 失敗後使用者沒 HITLCard 可按。
  2. `resume_approval_run` 非 atomic — ①+② 不在同一個 transaction,且 ② 的 `dispatch_run.delay()` 是 broker I/O 本來就不能 atomic。失敗時留下 `RUNNING` 但沒 task 的 zombie。
  3. 沒有殭屍 run 清理機制 — stuck 的 `RUNNING` / `AWAITING_APPROVAL` 無 auto-recover。
  4. `/approval/` 非 idempotent — retry 安全性差,重複呼叫可能進不一致狀態。

建議修法

短期(~30 分鐘,立刻補)

  • 前端:`resumeAgent` catch 時把 `pendingApproval` 還原,讓使用者可重試
    ```ts
    const savedRequest = pendingApproval;
    setPendingApproval(null);
    try { await resumeAgent(decision); }
    catch (err) { setPendingApproval(savedRequest); throw err; }
    ```
  • Backend:`resume_approval_run` 在 ② dispatch 失敗時把 run 撥回 `AWAITING_APPROVAL`
    ```python
    run.save(...)
    try:
    dispatch_run(run)
    except Exception:
    run.status = AWAITING_APPROVAL
    run.save(update_fields=["status", "updated_at"])
    raise
    ```
  • View:500 時回 `{"error": "...", "retryable": true}`,前端據此判斷是否自動 retry。

中期(1~2 小時)

  • 殭屍 run detector:Celery beat 每 5 分鐘掃 `RUNNING` 超過 15 分鐘且 `celery_task_id` 對應的 task state 是 `FAILURE`/`REVOKED`/不存在的 run,呼叫 `mark_run_failed`。
  • `/approval/` idempotent:若 run 已是 `RUNNING` 且 `resume_decision` 已記錄,直接回 200 同狀態,前端可安全 retry。

長期

  • 接 durable execution(Temporal / Inngest / DBOS)把 HITL signal-wait 從 app server 卸下,人類 pending 期間不占任何資源。可延伸討論。

相關檔案

  • `frontend/src/features/chatbot/hooks/useChatbot.ts`(`submitApproval`, `resumeAgent`)
  • `backend/apps/ai/services/run_runtime.py`(`resume_approval_run`, `dispatch_run`)
  • `backend/apps/ai/views.py`(`approval` action)
  • `backend/apps/ai/tasks.py`(若要加殭屍掃描)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions