feat(hitl): 增量3b —— 持久决定投递 + 生产激活(HITL 中断/恢复上线)
把中断/恢复模型在 dispatcher 接线启用,并让审批决定持久化抗离线。至此 HITL 从「阻塞 goroutine 等 5min、core NATS 非持久、不抗重启」升级为「持久化中断 + 决定持久投递 + 从 checkpoint 恢复续跑」。 - contract: 新增审批决定流 StreamApprovals(SUNDYNIX_APPROVALS)/通配 SubjectApprovalAll/消费者 ConsumerApprovals + checkpoint 桶 BucketCheckpoints。 - bus: EnsureApprovalStream(JetStream 流持久捕获 sundynix.approval.>,MaxAge 24h; gateway 现有 nc.Publish 的决定被本流自动捕获,无需改 gateway)+ ConsumeApprovals (持久消费者,队列组多副本安全)。 - orchestrator: HandleApprovalDecision(据 task_id 取 resume 记录续跑;无记录则忽略, 兼容阻塞态任务的决定 + 决定重投幂等)+ finishResumed(收尾对齐 Handle 尾段: 中断/拒绝/预算/失败/成功+评测落历史)。 - main: 开 checkpoint 存储 + 审批流 + 起决定消费者 → SetCheckpoints 启用中断模型; 任一步失败优雅降级回阻塞模型;停机 drain 在途 resume。 决定经 JetStream 持久:dispatcher 在决定发出时离线,重连后仍消费到并续跑;任一 dispatcher 副本都能据共享 KV 的 checkpoint+记录恢复(HA)。 测试:决定驱动恢复(续跑下游+判 done+清记录)、无记录忽略(幂等/兼容)。 全模块 go build + go test 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/sundynix/sundynix-dispatcher/internal/harness"
|
||||
"github.com/sundynix/sundynix-dispatcher/internal/llm"
|
||||
dnats "github.com/sundynix/sundynix-dispatcher/internal/nats"
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
"github.com/sundynix/sundynix-shared/otelx"
|
||||
"github.com/sundynix/sundynix-shared/secrets"
|
||||
)
|
||||
@@ -57,6 +58,23 @@ func main() {
|
||||
orch.SetGuardian(guardian) // 输入护栏 Tier2
|
||||
orch.SetUsageSink(sub) // 成本护栏:token 用量回写网关累计/计费
|
||||
|
||||
// HITL 持久化中断/恢复:开 checkpoint 存储 + 审批决定流,审批节点改走中断模型
|
||||
// (compose.Interrupt 落盘释放 goroutine、抗 dispatcher 重启)。任一步失败则降级回阻塞模型。
|
||||
var drainApprovals func(context.Context)
|
||||
if kv, kerr := sub.Checkpoints(context.Background(), contract.BucketCheckpoints, 24*time.Hour); kerr != nil {
|
||||
log.Printf("[dispatcher] 开 checkpoint 存储失败,审批降级为阻塞模型: %v", kerr)
|
||||
} else if serr := sub.EnsureApprovalStream(context.Background()); serr != nil {
|
||||
log.Printf("[dispatcher] 建审批决定流失败,审批降级为阻塞模型: %v", serr)
|
||||
} else {
|
||||
orch.SetCheckpoints(kv) // 启用中断模型
|
||||
if drain, cerr := sub.ConsumeApprovals(context.Background(), orch.HandleApprovalDecision); cerr != nil {
|
||||
log.Printf("[dispatcher] 启动审批决定消费者失败: %v", cerr)
|
||||
} else {
|
||||
drainApprovals = drain
|
||||
log.Println("[dispatcher] HITL 中断/恢复已启用(checkpoint + 审批决定流)")
|
||||
}
|
||||
}
|
||||
|
||||
// 健康心跳:dispatcher 无 HTTP/工具端点,挂一个 NATS 应答让管理端「服务状态」探到它在线。
|
||||
startedAt := time.Now()
|
||||
if unsub, herr := sub.ServeHealth(func() []byte {
|
||||
@@ -82,6 +100,12 @@ func main() {
|
||||
if err := sub.ConsumeTasks(ctx, orch.Handle); err != nil && err != context.Canceled {
|
||||
log.Fatalf("[dispatcher] exit: %v", err)
|
||||
}
|
||||
// 优雅停机:停审批决定消费者并等在途 resume 跑完(与任务 drain 同语义)。
|
||||
if drainApprovals != nil {
|
||||
dctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
drainApprovals(dctx)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
|
||||
Reference in New Issue
Block a user