Human-in-the-loop
Tổng quan
Làm cho loop có thể tạm dừng và tiếp tục từ trạng thái đã lưu. Cấu hình interrupt_on theo từng tool khiến một tool call khớp điều kiện được lưu thành một approval đang chờ; run chuyển sang awaiting_approval và worker nhả slot của nó. Sau đó một con người gọi POST /v1/runs/{id}/approvals với các quyết định approve, edit, reject hoặc respond, và run được đưa lại vào hàng đợi rồi tiếp tục đúng từ chỗ nó đã dừng. Đây là phase kiểm chứng mọi quyết định về độ bền (durability) trước đó: message append-only, state của run đã lưu, event log và việc sửa các tool call bị treo.
Yêu cầu
Chức năng
- Config
interrupt_ontheo từng agent:{"tool_name": true}hoặc{"tool_name": {"allowed_decisions": ["approve","reject"], "when": {...}}}.InterruptConfigđược định nghĩa ở phase 9; phase này mở rộng nó với mệnh đềWhen. - Các rule interrupt mặc định mà config của người dùng có thể nới lỏng nhưng không được âm thầm gỡ bỏ: `write_file` và `edit_file` nhắm vào `/memories/` hoặc `/skills/`. Chúng được gộp với config của người dùng, trong đó người dùng thắng theo từng tên tool, và muốn gỡ thì phải có `"interrupt_on": {"write_file": false}` tường minh, được log lúc lưu agent. Đây là thứ thay thế quy tắc tạm của phase 8 vốn biến các prefix đó thành chỉ dùng được qua API.
HumanInTheLoopMiddlewarenằm muộn trong stack: trongWrapToolCall, một call khớp điều kiện được lưu thành một dòngpending_approvalsvà middleware trả về một sentinel error làm run tạm dừng.- Thêm run state
awaiting_approval, vớiawaiting_sincevà một thời hạn cấu hình được (HANGO_APPROVAL_TTL, mặc định 24h), sau đó run bị một sweeper chuyển sangfailedvới lý do rõ ràng. - Mọi tool call trong cùng một batch được lưu cùng nhau: batch bị tạm dừng một cách nguyên tử, nên không tool nào trong batch chạy khi một tool anh em còn chờ approval.
- Các quyết định:
approve(chạy như đã yêu cầu),edit(chạy với đối số do con người cung cấp, và ghi lại cả hai),reject(trả về một tool error result mang thông điệp của con người và để model tiếp tục),respond(bỏ qua việc chạy và trả văn bản của con người làm tool result). - Đối số của một quyết định `edit` được **kiểm tra theo JSON Schema `Parameters()` của tool và chạy lại qua matcher `when` của rule** trước khi thực thi. Một con người (hoặc một session bị chiếm quyền) không được phép đưa cho tool những đối số mà model không bao giờ tạo ra được, cũng không được sửa một call ra ngoài phạm vi vốn làm nó bị interrupt ngay từ đầu. Kiểm tra thất bại thì trả 400 kèm lỗi schema và để approval ở trạng thái pending.
POST /v1/runs/{id}/approvalsnhận một quyết định cho mỗi call đang chờ, kiểm tra quyết định đó được phép với tool ấy, và — trong một transaction duy nhất — lưu các quyết định, chuyển runawaiting_approval → queuedcó điều kiện theo state hiện tại, rồi trả về. Việc enqueue diễn ra sau commit và được khử trùng theo run id. Duyệt một phần (mới quyết định một số call) trả 409 kèm danh sách còn lại. Vì vậy hai request đồng thời mà mỗi cái đều hoàn tất quyết định cuối cùng chỉ tạo ra đúng một lần chuyển state và đúng một lần thực thi; request thua thấy xung đột và trả 409.- Handler tự publish `approval.decided` qua broadcaster, để client đang xem stream SSE thấy quyết định ngay lập tức dù lúc đó không có worker nào đang giữ run. Chờ worker đã resume phát nó sẽ để lại một khoảng trống nhìn thấy được từ vài giây đến vài phút.
GET /v1/runs/{id}/approvalsliệt kê các call đang chờ cùng đối số của chúng;GET /v1/approvals?state=pendingliệt kê hàng đợi của một tenant.- SSE phát
approval.requested(kèm tên tool, đối số, các quyết định được phép) vàapproval.decided; stream vẫn mở xuyên suốt lúc tạm dừng để client đang xem thấy được sự chuyển tiếp. - Subagent kế thừa: subagent khai báo (declarative) kế thừa
interrupt_oncủa cha trừ khi tự khai báo riêng; một lần tạm dừng của subagent làm tạm dừng cả run, và việc resume đi vào lại child tại call đang chờ.
Phi chức năng
- **Thứ tự resume là nạp quyết định rồi mới đến `BeforeRun`.** Worker nạp các approval `decided` của run vào `State.Private` *trước khi* các hook `BeforeRun` của chain chạy. Khi đó `RepairToolCallsMiddleware` **bỏ qua mọi `tool_call_id` có một pending approval đã được quyết định**: các call ấy không phải đang bị treo, chúng sắp được thực thi. Nếu không có thứ tự này, bước repair sẽ thấy các block `tool_use` chưa có kết quả của batch đang tạm dừng, tổng hợp các kết quả lỗi "tool call interrupted" cho chúng, và approval mà con người vừa cấp bị phá hủy trước khi được dùng.
- **Resume chạy lại batch đang chờ qua tool chain**; nó không đi vào lại ở một lời gọi model. Điểm vào resume của loop nhận batch đang chờ, chạy từng call qua `Chain.ToolCall` (nơi `HITLMiddleware` tìm thấy quyết định và áp dụng nó), lưu message tool-result thu được, và chỉ sau đó mới tiếp tục sang lời gọi model kế tiếp. Đi vào lại ở một lời gọi model sẽ hoặc hỏi lại model (nhân đôi batch) hoặc gửi một request mà message assistant cuối cùng còn các block `tool_use` chưa khớp.
Việc resume không được chạy lại các tool call đã hoàn tất: loop được resume dựng lại state từ các message đã lưu cộng
runs.state_json, và middleware repair chỉ điền các call thực sự đang bị treo và chưa được quyết định.Slot của worker được nhả trong lúc tạm dừng; một run bị tạm dừng không giữ goroutine, không giữ entry registry trong bộ nhớ và không giữ kết nối database nào.
- Ma trận phân quyền approval: chỉ `owner` hoặc `admin` của tenant chứa run mới được quyết định, **và không bao giờ là người tạo ra run bị interrupt trừ khi người đó là `owner` hoặc `admin`**. Tự duyệt làm vô hiệu toàn bộ cơ chế kiểm soát: ý nghĩa của một interrupt là một bên thứ hai review một hành động có đặc quyền. Một `member` — kể cả người tạo run — luôn nhận 403 ở `POST /v1/runs/{id}/approvals`. `GET /v1/runs/{id}/approvals` theo quy tắc hiển thị run thông thường (giới hạn theo người tạo với `member`), nên người tạo thấy được cái gì đang chờ mà không quyết định được. Mọi quyết định đều ghi `decided_by`.
- Mọi kết quả approval đều được audit, chứ không chỉ log. Một quyết định của con người ghi một dòng `audit_events` với `actor_kind = 'user'`, action `approval.decide`, `target_kind = 'pending_approval'`, id của approval làm `target_id`, và `detail` mang `decided_by`, loại quyết định, tên tool và `tool_call_id` — không bao giờ mang đối số. Một lần hết hạn do sweeper ghi ra cùng hình dạng đó với `actor_kind = 'system'`, `actor_id` null và action `approval.expire`, vì "không ai quyết định, và hệ thống đã bỏ cuộc" chính là câu trả lời mà người review cần khi một run thất bại qua đêm. Cả hai dòng đều mang `trace_id` của trace đã sinh ra chúng, nên chúng nối với các trace của run bị tạm dừng và run được resume.
| Route | owner / admin | member (gồm cả người tạo run) |
|---|---|---|
GET /v1/runs/{id}/approvals | cho phép (mọi run trong tenant) | cho phép (chỉ run của mình) |
GET /v1/approvals?state=pending | cho phép (hàng đợi của tenant) | cho phép (chỉ run của mình) |
POST /v1/runs/{id}/approvals | cho phép | từ chối (403) |
- Test cô lập chéo tenant và trong cùng tenant cho
pending_approvals.
Kiến trúc
// internal/agent/mwhitl.go
// InterruptConfig is declared in phase 9 (internal/agent/subagent.go); this phase adds When.
// Red team: F15
type InterruptConfig struct {
AllowedDecisions []string `json:"allowed_decisions,omitempty"` // default all four
When *WhenClause `json:"when,omitempty"` // arg matchers
}
type Decision struct {
Kind string // approve|edit|reject|respond
Args json.RawMessage // edit only
Message string // reject|respond
DecidedBy uuid.UUID
}
type HITLMiddleware struct {
Config map[string]InterruptConfig
Store store.ApprovalStore
} // ToolWrapper
var ErrAwaitingApproval = errors.New("run suspended awaiting approval")
// internal/agent/loop.go (extended)
type Status uint8 // StatusDone, StatusFailed, StatusCancelled, StatusAwaitingApproval
// Run returns StatusAwaitingApproval without error; the worker persists and releases the slot.
// Red team: F3 — on resume the worker loads decisions into State.Private BEFORE Chain.BeforeRun,
// RepairToolCalls skips decided tool_call_ids, and the loop re-drives the pending batch through
// Chain.ToolCall rather than re-entering at a model call.-- migrations/00010_pending_approvals.sql
create table pending_approvals (
id uuid primary key,
tenant_id uuid not null references tenants(id) on delete cascade,
run_id uuid not null references runs(id) on delete cascade,
turn int not null,
tool_call_id text not null,
tool_name text not null,
arguments jsonb not null,
allowed_decisions text[] not null,
subagent_call_id uuid references subagent_calls(id) on delete cascade,
state text not null check (state in ('pending','decided','expired')),
decision text not null default '',
decision_args jsonb,
decision_message text not null default '',
decided_by uuid references users(id),
decided_at timestamptz,
created_at timestamptz not null default now(),
unique (run_id, tool_call_id)
);
create index pending_approvals_tenant_state_idx on pending_approvals (tenant_id, state, created_at);
create index pending_approvals_tenant_run_idx on pending_approvals (tenant_id, run_id);
alter table runs drop constraint runs_state_check;
alter table runs add constraint runs_state_check check (state in ('queued','running','awaiting_approval','succeeded','failed','cancelled'));
alter table runs add column awaiting_since timestamptz;Instrumentation
- Việc tạm dừng được thể hiện tường minh trong trace: span
execute_toolkết thúc vớihango.tool.outcome=awaiting_approvalvà một span eventtool.approval_requestedmang tên tool, call id và các quyết định được phép; spaninvoke_agentkết thúc vớihango.run.outcome=awaiting_approvalthay vì mở suốt nhiều giờ. - Việc resume bắt đầu một trace mới mà span gốc
invoke_agentmang một span link tới span gốc của run bị tạm dừng cùnghango.run.resumed_from_span_idvàhango.run.suspension_seconds; cả hai trace dùng chunglangfuse.session.idvàlangfuse.trace.metadata.run_idđể Langfuse nhóm chúng dưới một run. - Span event
tool.approval_decidedvớihango.approval.decisionvà id user quyết định; các quyết địnheditghi lại rằng đối số đã đổi nhưng không ghi nội dung đối số trừ khi bật content tracing. - Metric: gauge
hango.approvals.pending(attributetool), histogramhango.approvals.wait_time, counterhango.approvals.decisions(attributedecision,tool), counterhango.approvals.expired,hango.runs.by_statecó thêm stateawaiting_approval. - Trường log: việc yêu cầu và quyết định approval được log ở mức info kèm run id, tool, call id, decision và user quyết định — dấu vết audit mà người review thực sự sẽ đọc.
Các file liên quan
Tạo mới
internal/agent/mwhitl.go,internal/agent/mwhitl_test.go,internal/agent/when.go(các matcher đối số) và test của nó.internal/store/pg/approval.go,internal/store/pg/approval_test.go.internal/server/handlers_approvals.go.internal/run/approval_sweeper.go— làm hết hạn các pending approval cũ và làm thất bại các run của chúng.migrations/00010_pending_approvals.sql.docs/learning/15-human-in-the-loop.md.
Sửa
internal/agent/loop.go—StatusAwaitingApproval, tiêu thụ quyết định trước khi tạm dừng, tạm dừng nguyên tử theo batch.internal/agent/mwstack.go— điền slot HumanInTheLoop (sau Memory, trước allowlist) và gộp các rule interrupt của filesystem với config của người dùng, người dùng thắng theo từng tên tool; cập nhật test thứ tự đã ghim.internal/agent/childbuilder.go— subagent kế thừainterrupt_on.internal/run/worker.go— lưuawaiting_approval, nhả slot, enqueue lại khi có quyết định; đường resume nạp các quyết định.internal/run/emitter.go— các loại eventapproval.requested/approval.decided.internal/server/handlers_run_events.go— giữ stream mở xuyên suốt lúc tạm dừng thay vì đóng khi gặp một state không phải running.internal/store/pg/run.go— state mới vàawaiting_since.internal/config/config.go—HANGO_APPROVAL_TTL,HANGO_APPROVAL_SWEEP_INTERVAL.internal/store/pg/tenancy_test.go— thêmpending_approvals(chéo tenant và trong cùng tenant).internal/server/authz.go— các dòng approvals của ma trận phân quyền.deploy/grafana/provisioning/alerting.yaml,deploy/grafana/dashboards/hango-runs.json— alert và panel của approvals.docs/system-architecture.md,docs/learning/README.md.
Các bước triển khai
- Viết
migrations/00010_pending_approvals.sql, gồm cả việc đổi constraint củaruns.statevàawaiting_since. - Viết
ApprovalStorecó scope:CreateBatch,ListPending(runID),ListPendingByTenant,Decide(id, decision, args, message, userID),ExpireOlderThan. - Viết
internal/agent/when.go: một matcher nhỏ hỗ trợ khớp chính xác và khớp prefix trên các trường đối số cộng một ngưỡng kích thước, để các rule như “chỉ interruptwrite_filedưới/memories/” diễn đạt được mà không cần code. - Viết
internal/agent/mwhitl.go: trongWrapToolCall, trước hết kiểm traState.Privatexem đã có quyết định nào được nạp chotool_call_idnày chưa. Nếu có thì áp dụng nó (approve→next;edit→ kiểm tra đối số được cung cấp theotool.Parameters()và chạy lại matcherwhencủa rule, rồinextvới đối số đã thay, ghi lại cả hai bộ đối số;reject→ error result kèm thông điệp;respond→ result không lỗi kèm thông điệp) và ghi span event. Nếu không có và tool khớp một rule interrupt, đăng ký call vào danh sách pending của batch và trảErrAwaitingApproval. - Làm cho việc tạm dừng nguyên tử theo batch trong
loop.go: chạy các tool call của batch qua chain, thu thập hoặc kết quả hoặc các đăng ký pending; nếu có bất kỳ đăng ký nào, lưu toàn bộ các dòng pending trong một transaction, lưuStatevàoruns.state_json, và trảStatusAwaitingApprovalmà không lưu tool result từng phần nào của batch. - Mở rộng worker: khi gặp
StatusAwaitingApproval, đặt run thànhawaiting_approvalvớiawaiting_since, phát các eventapproval.requested, kết thúc span của run với outcome tạm dừng, và nhả slot của lane. - Viết các handler approval: phân quyền theo ma trận ở trên (chỉ
owner/adminđược quyết định, không bao giờ làmemberkể cả khi là người tạo run), kiểm tra quyết định theoallowed_decisions, kiểm tra schema đối sốeditvàwhen, kiểm tra tính đầy đủ kiểu tất-cả-hoặc-không trả 409 kèm các call còn lại, rồi một transaction lưu các quyết định và chuyển runawaiting_approval → queuedcó điều kiện; enqueue sau commit (có khử trùng). Publishapproval.decidedqua broadcaster từ handler. GọiSetAuditAction(c, "approval.decide", "pending_approval", approvalID)vớidecided_by, loại quyết định, tên tool vàtool_call_idtrongdetail; một 403 hoặc 409 cũng được audit, với outcomedeniedhoặcerror. - Mở rộng đường resume: worker nạp các approval đã quyết định của run vào
State.PrivatetrướcChain.BeforeRun,RepairToolCallsMiddlewarebỏ qua cáctool_call_idcó approval đã quyết định, và loop chạy lại batch đang chờ quaChain.ToolCalltrước lời gọi model kế tiếp. Một tool call chưa bao giờ được quyết định (ví dụ sau một lần thất bại do hết hạn) vẫn được repair như bình thường. - Viết
internal/run/approval_sweeper.gonhư một tickersafegolàm hết hạn các approval quá TTL. Mỗi lần hết hạn là một transaction: đánh dấu các approval làexpiredvà chuyển run sangfailedcó điều kiện rằng nó vẫn đangawaiting_approval, để một quyết định đến cùng lúc hoặc thắng hoặc thấy xung đột — không bao giờ cả hai. Phát các event kết thúc sau commit. Sweeper không có request nên không có middleware: nó tự thêm trực tiếp một dòngaudit_eventscho mỗi approval hết hạn, vớiactor_kind = 'system'và actionapproval.expire. - Nối việc subagent kế thừa trong child builder và xác minh rằng một lần tạm dừng bên trong child làm tạm dừng run cha với
subagent_call_idđược ghi lại. - Giữ SSE handler mở xuyên suốt lúc tạm dừng: coi
awaiting_approvallà trạng thái chưa kết thúc và tiếp tục stream. - Thêm instrumentation, gồm cả span link khi resume.
- Test: interrupt kích hoạt tạm dừng và không tool nào trong batch đó chạy; mỗi quyết định trong bốn loại tạo ra đúng tool result và run hoàn tất; một tool đã được approve chạy đúng một lần khi resume, khẳng định bằng một fake tool có đếm, và chứng minh repair không thay thế nó (test hồi quy phá hủy approval) ; hai request đồng thời mà mỗi cái hoàn tất quyết định cuối cùng chỉ tạo ra đúng một lần chuyển run và đúng một lần thực thi ; một quyết định chạy đua với expiry sweeper ra đúng một kết quả; duyệt một phần trả 409; một
member, kể cả người tạo run, nhận 403 khi quyết định và 200 khi list cho run của mình ; một quyết địnheditvới đối số vi phạm schema của tool trả 400 và để approval ở pending, còn quyết định đẩy call ra ngoài phạm viwhenthì bị từ chối ; matcherwhengiới hạn một interrupt vào một prefix đường dẫn; mộtwrite_filevào/memories/bị interrupt theo mặc định ; hết hạn làm run thất bại; client SSE quan sátapproval.requestedrồiapproval.decideddo handler publish trên một kết nối; tạm dừng và resume của subagent; các ca chéo tenant và trong cùng tenant củapending_approvals. - Thêm các alert rule của approvals vào
deploy/grafana/provisioning/alerting.yaml(gauge pending tăng, tỉ lệ hết hạn) và các panel approvals vàohango-runs. - Cập nhật tài liệu và viết tài liệu học.
Dàn ý tài liệu học
docs/learning/15-human-in-the-loop.md phải có:
- Khái niệm: vì sao HITL buộc loop phải là một state machine có thể resume chứ không phải một hàm chặn (blocking), và vì sao mọi quyết định về độ bền trước đó chính là thứ làm cho việc này rẻ.
- Lựa chọn thiết kế: interrupt là một
ToolWrappertrả về một sentinel, tạm dừng nguyên tử theo batch, bốn loại quyết định, quyết định được nạp vào state và được chính middleware đó tiêu thụ, nhả slot trong lúc tạm dừng, expiry sweeper, span link qua ranh giới tạm dừng. - Đánh đổi: sentinel error so với trả về status từ chain; tạm dừng cả batch so với chạy trước các call anh em an toàn (chọn nguyên tử: chạy từng phần làm ngữ nghĩa của reject không thể xác định); polling so với SSE để thông báo approval; hết hạn theo TTL so với chờ vô thời hạn.
- Vì sao state machine của run giờ có sáu state và cái gì canh gác mỗi lần chuyển.
- So sánh với goclaw: approval tồn tại như một setter interface
ApprovalAwaretrên các tool với wiring trong tiến trình, không phải suspend/resume được lưu bền, nên restart làm mất các approval đang chờ. - So sánh với deepagents:
interrupt_onvớiapprove|edit|reject|respond,HumanInTheLoopMiddlewarenằm muộn trong stack,FilesystemPermission{mode:"interrupt"}tự sinh các entry gộp với config của người dùng (người dùng thắng theo từng tên tool), yêu cầu có checkpointer, resume quaCommand(resume={"decisions":[...]})trên cùngthread_id, và các subagent khai báo kế thừainterrupt_oncòn các subagent đã compile thì không bao giờ. - Vì sao hango đi khác: approval là các dòng có chủ sở hữu, có audit trail và có thời hạn, không phải state của checkpoint; resume là một lời gọi API có xác thực, không phải một command object phía client.
- Cách debug tầng này:
hango.approvals.pendingtheo tool cho thấy con người đang bị chặn ở đâu;hango.approvals.wait_timecho thấy hàng đợi có đang được xử lý hay không; hai trace được link (bị tạm dừng và được resume) cho thấy chính xác con người đã đổi gì, vì các quyết địnheditđược ghi cả dưới dạng span event lẫn dạng dòng; Loki lọc theohango.run.idcho toàn bộ câu chuyện audit kể cả user quyết định. - Ràng buộc thứ tự resume (quyết định → repair → chạy lại batch) là điều tinh vi nhất trong toàn bộ roadmap, và mỗi thứ tự sai làm hỏng cái gì.
- Bài tập: cấu hình
write_fileđể interrupt, duyệt một call và sửa một call khác, rồi reject call thứ ba và xem model phục hồi; thử tự duyệt run của mình với tư cáchmembervà đọc lỗi 403. - Vì sao log approval chưa đủ và dòng audit mới là artifact: một dòng log là best-effort, không có index và chịu chính sách retention; một dòng `audit_events` query được theo tenant và target, nối được với trace của nó, và sống sót qua pipeline log.
- Ngân sách thời gian: 12h cài đặt + 2h tài liệu học.
Tiêu chí thành công
-
go build ./... && go vet ./... && go test ./...đều qua. - Một tool được cấu hình làm run tạm dừng sang
awaiting_approvalvà không tool nào trong batch đó đã chạy (khẳng định bằng fake tool có đếm). - Mỗi quyết định trong
approve,edit,reject,respondresume run và tạo ra đúng tool result đã mô tả (bốn test). - Duyệt một phần trả 409 liệt kê các call còn lại; một
member— kể cả người tạo run — nhận 403 khi quyết định. - Một tool đã được approve chạy đúng một lần sau khi resume (fake tool có đếm), và
RepairToolCallskhông thay nó bằng một kết quả “interrupted”. - Hai approval cuối cùng đồng thời chỉ tạo ra đúng một lần chuyển run và đúng một lần thực thi; request thua nhận 409.
- Một quyết định
editvới đối số sai schema trả 400 và để approval ở pending; quyết định thoát khỏi phạm viwhencủa rule thì bị từ chối. - Một run bị tạm dừng không chiếm slot lane nào và không chiếm goroutine nào (test rò rỉ cộng khẳng định thống kê lane).
- Một run được resume không chạy lại các tool call đã hoàn tất (test).
- Expiry sweeper làm một run cũ thất bại với
"approval expired"và phátrun.finished. - Mọi kết quả approval đều được audit: một quyết định
approve/edit/reject/respondghi một dòngapproval.decidemangdecided_byvà loại quyết định; một 403 củamemberghi một dòng với outcomedenied; một lần hết hạn do sweeper ghi một dòngapproval.expirevớiactor_kind = 'system'. Không dòng nào chứa đối số của tool. - Một kết nối SSE quan sát
approval.requested, rồiapproval.decideddo handler publish ngay lúc quyết định (không phải vài phút sau khi worker nhận run), rồi phần còn lại của run. - Khi không có config nào của người dùng, một
write_filevào/memories/AGENTS.mdtạm dừng để chờ approval. - Một interrupt ở cấp subagent làm tạm dừng run cha với
subagent_call_idđược đặt và resume vào lại child. - Trong Tempo, các trace bị tạm dừng và được resume được link với nhau và
hango.run.suspension_secondsđược ghi lại; trong Langfuse cả hai xuất hiện dưới cùng một session; các serieshango.approvals.*tồn tại.
Đánh giá rủi ro
| Rủi ro | Khả năng x Tác động | Tín hiệu | Cách xử lý đã định sẵn |
|---|---|---|---|
| Resume chạy lại các tool có tác dụng phụ | Trung bình x Nghiêm trọng | Ghi trùng sau khi approve | Tạm dừng nguyên tử theo batch nghĩa là không kết quả nào của batch đó được lưu, và các quyết định được khóa theo tool_call_id; test fake tool có đếm là cổng chặn |
| Run tạm dừng mãi mãi và làm rò rỉ quota/sự chú ý | Cao x Trung bình | hango.approvals.pending tăng không giới hạn | Expiry sweeper theo TTL cộng gauge pending trên dashboard và một alert trong bộ rule của phase 14 |
| Tool call bị treo sau một lần thất bại do hết hạn | Cao x Cao | Provider trả 400 ở run kế tiếp của session đó | Middleware repair (phase 7) tổng hợp error tool result chỉ cho các call chưa được quyết định; một test chạy hết hạn rồi một run mới trong cùng session |
| Repair phá hủy batch mà nó lẽ ra phải resume | Trung bình x Nghiêm trọng | Tool đã approve không bao giờ chạy; model thấy “tool call interrupted” | Quyết định được nạp trước BeforeRun, repair bỏ qua các tool_call_id đã quyết định, và resume chạy lại batch qua tool chain; test exactly-once là cổng chặn |
| Phân quyền quyết định quá lỏng | Trung bình x Cao | Một user duyệt chính hành động có đặc quyền của mình | Chỉ owner/admin, và không bao giờ là người tạo run trừ khi họ giữ một trong các vai trò đó; thực thi trong authz.go, có test, và mọi quyết định đều ghi decided_by |
| Một quyết định và một lần hết hạn, hoặc hai quyết định, chạy đua | Trung bình x Cao | Một run chạy hai lần, hoặc vừa hết hạn vừa được resume | Quyết định và hết hạn đều chuyển run có điều kiện trong cùng transaction với lần ghi của chính chúng; test approval đồng thời là một tiêu chí nghiệm thu |
| Tạm dừng lâu làm đứt tính liên tục của trace | Trung bình x Thấp | Trace mồ côi trong Tempo | Span được đóng lúc tạm dừng và trace được resume mang một span link cộng session id Langfuse dùng chung |
| Kết nối SSE nhàn rỗi hàng giờ trong lúc tạm dừng | Trung bình x Trung bình | Proxy giết stream | Các ping keepalive tiếp tục trong lúc tạm dừng; client được dặn kết nối lại với Last-Event-ID, cách này vẫn dùng được vì event log bền vững |
Rollback
goose down một bước xóa pending_approvals, runs.awaiting_since và khôi phục constraint năm state (mọi run đang ở awaiting_approval phải được chuyển sang failed trước; migration down làm việc này một cách tường minh). Gỡ slot HITL khỏi DefaultStack vô hiệu hóa hoàn toàn các interrupt và loop trở về hành vi của phase 14.