diff --git a/sundynix-desktop/frontend/src/components/ExecTrace.tsx b/sundynix-desktop/frontend/src/components/ExecTrace.tsx index e4f6b1c..2aae4c3 100644 --- a/sundynix-desktop/frontend/src/components/ExecTrace.tsx +++ b/sundynix-desktop/frontend/src/components/ExecTrace.tsx @@ -43,7 +43,13 @@ function StatusDot({ status }: { status: NodeTrace["status"] }) { // ExecTrace 把执行事件流渲染为竖向轨道:每个节点一颗灯,实时点亮 + 耗时 + 入参/产出。 export function ExecTrace({ events, phase, compact }: { events: ExecEvent[]; phase?: RunPhase; compact?: boolean }) { - const nodes = deriveNodes(events); + const raw = deriveNodes(events); + // 运行整体已完成,但轨迹可能缺了中断后(resume)续录的事件(如审批通过、续跑节点), + // 导致审批节点停在 waiting 转圈。既然已完成,把悬挂的 waiting/running 收敛为 done,不再假装进行中。 + const nodes = + phase === "done" + ? raw.map((n) => (n.status === "waiting" || n.status === "running" ? { ...n, status: "done" as const } : n)) + : raw; if (nodes.length === 0) { return (
diff --git a/sundynix-dispatcher/internal/eino/exec.go b/sundynix-dispatcher/internal/eino/exec.go index a836741..dfae5ff 100644 --- a/sundynix-dispatcher/internal/eino/exec.go +++ b/sundynix-dispatcher/internal/eino/exec.go @@ -17,9 +17,10 @@ type ExecSink interface { // execTracer 为一个任务发结构化执行事件,自增 Seq 保序、span 自动计耗时。 type execTracer struct { - sink ExecSink - task string - seq int32 + sink ExecSink + task string + seq int32 + suspended bool // HITL 中断挂起:done() 不关 exec 流,留给 resume 续录(对齐 token 流的中断处理) } // tracer 为某任务建一个事件发射器(sink 为空时所有方法变空操作)。 @@ -60,8 +61,10 @@ func (e *execTracer) span(node, kind, label string) func(detail string, err erro } // done 关闭该任务的执行事件流(让 SSE 客户端收尾)。 +// 中断挂起时不关:任务停在审批、resume 是另一次调用,过早关流会让录制器错过 resume 的轨迹事件 +// (审批通过/拒绝、续跑的节点),导致复盘里审批节点永远停在「等待中」。 func (e *execTracer) done() { - if e == nil || e.sink == nil { + if e == nil || e.sink == nil || e.suspended { return } _ = e.sink.CompleteExec(e.task) diff --git a/sundynix-dispatcher/internal/eino/orchestrator.go b/sundynix-dispatcher/internal/eino/orchestrator.go index 851f89a..9c9767f 100644 --- a/sundynix-dispatcher/internal/eino/orchestrator.go +++ b/sundynix-dispatcher/internal/eino/orchestrator.go @@ -266,7 +266,8 @@ func (o *Orchestrator) Handle(ctx context.Context, t *contract.Task) error { if errors.Is(err, errInterrupted) { // HITL 持久化中断:checkpoint 已落、任务停在 waiting(状态在审批节点内已置)。 // 释放 goroutine 但不收尾——不 CompleteStream、不评测、不判 done、不计熔断失败; - // SSE 流保持打开,UI 继续显示待审。人工决定到达后由 resume 续跑(增量3)。 + // token 流与 exec 流都保持打开(suspended),resume 时续录,否则复盘里审批节点永停在等待中。 + tr.suspended = true slog.InfoContext(ctx, "task interrupted for approval (checkpointed)", "task_id", t.ID) o.breaker.Report(true) // 中断是正常暂停,非后端故障 return nil