    case "agent.task": {
      // === 自主任务模式 v2 (Orchestrator + DAG + progress broadcast) ===
      const { chatId, agentId, goal } = event;
      if (!chatId || !agentId || !goal) {
        broadcast({ type: "error", message: "agent.task needs chatId/agentId/goal" });
        break;
      }

      const taskMsgId = 'task-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
      const agentSenderId = 'agent:' + agentId;

      // 1. broadcast task start
      broadcast({
        type: "agent.streaming.start",
        chatId,
        from: agentSenderId,
        senderType: "agent",
        msgId: taskMsgId,
        goal,
        timestamp: Date.now(),
      });

      // 2. async: plan -> orchestrate -> progress
      (async () => {
        // ⭐ 停止生成：注册后用户点"停止"可中断整个任务（否则任务无法被停止）
        const taskCtrl = new AbortController();
        registerGeneration(chatId, taskCtrl);
        let taskHeartbeatId: ReturnType<typeof setInterval> | undefined;
        try {
          // Phase 1: task decomposition
          broadcast({
            type: "task.progress",
            chatId, from: agentSenderId, msgId: taskMsgId,
            phase: "planning",
            message: "正在分析任务并制定执行计划...",
            step: 0, total: 0,
            timestamp: Date.now(),
          });

          // ⭐ 心跳保活：任务执行期间每 5 秒发一次心跳
          taskHeartbeatId = setInterval(() => {
            broadcast({
              type: "agent.streaming.heartbeat",
              chatId, from: agentSenderId, msgId: taskMsgId,
              timestamp: Date.now(),
            });
          }, 3000);
          if (taskHeartbeatId.unref) taskHeartbeatId.unref();

          const planPrompt = "你是灵犀引擎的任务规划器。请将以下任务拆分为 2-6 个可执行的子步骤。\n\n任务目标：" + goal + "\n\n要求：\n- 返回纯 JSON 数组，不要有其他文字\n- 每个步骤包含：id（step-1, step-2...）、name（简短名称）、prompt（具体执行指令）、dependsOn（依赖的前置步骤 id 数组，无依赖则为空数组）\n- 步骤之间可以有依赖关系（DAG），无依赖的步骤将并行执行\n- 第一步通常无依赖，最后一步通常是汇总\n\n示例格式：\n[{\"id\":\"step-1\",\"name\":\"搜索\",\"prompt\":\"搜索XX\",\"dependsOn\":[]},{\"id\":\"step-2\",\"name\":\"分析\",\"prompt\":\"分析结果\",\"dependsOn\":[\"step-1\"]}]\n\n请返回 JSON：";

          // 规划阶段：无工具一次性调用（修复：原先把 planPrompt 传到 generateReply 的 taskGoal 参数位，
          // 被包成“自主执行任务”模式与“只返回 JSON”指令冲突，且会误触发工具循环）
          const planResponse = await router.generateOneShot(agentId, planPrompt, { temperature: 0.2, timeoutMs: 300000 });

          // parse JSON plan (tolerant: extract first JSON array)
          let steps: any[] = [];
          try {
            const jsonMatch = planResponse.match(/\[[\s\S]*\]/);
            if (jsonMatch) {
              steps = JSON.parse(jsonMatch[0]);
            }
          } catch { /* parse error */ }

          // fallback: single step if parse failed
          if (!steps || !Array.isArray(steps) || steps.length === 0) {
            steps = [{ id: 'step-1', name: '执行任务', prompt: goal, dependsOn: [] }];
          }

          // ⭐ 计划清洗（修复拆分瑕疵）：
          // 1) 丢弃无 prompt 的无效步骤；2) 补齐/去重 id；3) 依赖引用去重
          steps = steps.filter((s: any) => s && typeof s.prompt === 'string' && s.prompt.trim());
          if (steps.length === 0) {
            steps = [{ id: 'step-1', name: '执行任务', prompt: goal, dependsOn: [] }];
          }
          const seenIds = new Set<string>();
          steps = steps.map((s: any, i: number) => {
            let id = (typeof s.id === 'string' && s.id.trim()) ? s.id.trim() : ('step-' + (i + 1));
            while (seenIds.has(id)) id = id + '-' + Math.random().toString(36).slice(2, 5);
            seenIds.add(id);
            return {
              ...s,
              id,
              name: (typeof s.name === 'string' && s.name.trim()) ? s.name.trim() : ('步骤 ' + (i + 1)),
              dependsOn: Array.isArray(s.dependsOn) ? [...new Set(s.dependsOn.filter((d: any) => typeof d === 'string'))] : [],
            };
          });

          // safety limit（截断后必须同步清理依赖引用，否则被引用步骤永远无法满足依赖 → 任务整体挂死）
          if (steps.length > 10) steps = steps.slice(0, 10);
          const validIds = new Set<string>(steps.map((s: any) => s.id));
          for (const s of steps as any[]) {
            s.dependsOn = s.dependsOn.filter((d: string) => validIds.has(d) && d !== s.id);
          }

          // save task plan (for resume support)
          const plan: TaskPlan = {
            taskId: taskMsgId,
            chatId, agentId, goal,
            steps: steps.map((s: any) => ({ ...s, status: 'pending' })),
            currentStep: 0,
            status: 'executing',
            createdAt: Date.now(),
          };
          activeTaskPlans.set(taskMsgId, plan);

          // Phase 2: broadcast plan
          broadcast({
            type: "task.progress",
            chatId, from: agentSenderId, msgId: taskMsgId,
            phase: "plan_ready",
            message: "任务已拆分为 " + steps.length + " 个步骤",
            step: 0, total: steps.length,
            steps: steps.map((s: any) => ({ id: s.id, name: s.name, dependsOn: s.dependsOn })),
            timestamp: Date.now(),
          });

          // Phase 3: execute DAG via Orchestrator
          const executor = async (task: TaskNode, context: Record<string, string>, signal?: AbortSignal) => {
            const stepIdx = steps.findIndex((s: any) => s.id === task.id);
            // 用户在步骤间隙点停止 → 直接失败，级联跳过后续依赖步骤
            if (taskCtrl.signal.aborted) {
              throw new Error('用户已停止任务');
            }
            let abortedByTimeout = false;
            const onAbort = () => { abortedByTimeout = true; };
            if (signal) signal.addEventListener('abort', onAbort, { once: true });

            broadcast({
              type: "task.progress",
              chatId, from: agentSenderId, msgId: taskMsgId,
              phase: "step_start",
              message: "执行步骤 " + (stepIdx + 1) + "/" + steps.length + "：" + task.name,
              step: stepIdx + 1, total: steps.length,
              stepId: task.id, stepName: task.name,
              timestamp: Date.now(),
            });

            // build prompt with dependency outputs
            let fullPrompt = task.prompt;
            const depOutputs = Object.entries(context);
            if (depOutputs.length > 0) {
              fullPrompt += "\n\n## 前置步骤结果\n";
              for (const [depId, depOutput] of depOutputs) {
                const depStep = steps.find((s: any) => s.id === depId);
                fullPrompt += "### " + (depStep?.name || depId) + "\n" + depOutput.slice(0, 3000) + "\n\n";
              }
            }

            let stepResult = '';
            let stepSuccess = true;
            // ⭐ 合并超时信号 + 用户停止信号，两者都能中断 AI 调用
            const merged = mergeAbort(signal, taskCtrl.signal);
            try {
              // 修复：fullPrompt 传到 customPrompt（第8参）而非 taskGoal（第6参）；
              // skipPersist 避免中间步骤污染聊天历史
              stepResult = await router.generateReply(
                chatId, agentId, taskMsgId,
                (chunk) => {
                  broadcast({ type: "agent.streaming.chunk", chatId, from: agentSenderId, msgId: taskMsgId, chunk });
                },
                // 推理过程（与主聊天路径一致，前端 .msg-think 区渲染）
                (thinkChunk: string) => {
                  broadcast({ type: "agent.streaming.think", chatId, from: agentSenderId, msgId: taskMsgId, think: thinkChunk });
                },
                undefined,
                (event: any) => {
                  broadcast({ ...event, chatId, from: agentSenderId, msgId: taskMsgId });
                },
                fullPrompt,
                merged.signal,
                { skipPersist: true }
              );
            } catch (stepErr: any) {
              stepSuccess = false;
              if (taskCtrl.signal.aborted) {
                stepResult = "步骤被用户停止中断";
              } else if (abortedByTimeout || signal?.aborted) {
                stepResult = "步骤执行超时（" + (task.timeout || ORCHESTRATOR_DEFAULT_TIMEOUT_MS) + "ms）被中断";
              } else {
                stepResult = "步骤执行失败：" + (stepErr.message || '未知错误');
              }
              console.error('[WS] step ' + task.id + ' failed:', stepErr);
            }

            if (signal) signal.removeEventListener('abort', onAbort);

            broadcast({
              type: "task.progress",
              chatId, from: agentSenderId, msgId: taskMsgId,
              phase: "step_done",
              message: (stepSuccess ? "步骤 " : "步骤失败 ") + (stepIdx + 1) + "：" + task.name,
              step: stepIdx + 1, total: steps.length,
              stepId: task.id, stepName: task.name,
              success: stepSuccess,
              output: (stepResult || "").slice(0, 500),
              timestamp: Date.now(),
            });

            // update plan state
            const p = activeTaskPlans.get(taskMsgId);
            if (p) {
              const st = p.steps.find(s => s.id === task.id);
              if (st) { st.status = stepSuccess ? 'done' : 'failed'; st.output = stepResult; }
              p.currentStep = stepIdx + 1;
              if (!stepSuccess) p.status = 'failed';
            }

            return stepResult || '';
          };

          const orchestrator = new Orchestrator(executor);
          for (const step of steps) {
            orchestrator.addTask({
              id: step.id,
              name: step.name,
              prompt: step.prompt,
              dependsOn: step.dependsOn || [],
              timeout: ORCHESTRATOR_DEFAULT_TIMEOUT_MS, // v2.1: 统一引用 orchestrator 导出的默认值，消除 120s/300s 分歧
            });
          }

          const orchResult = await orchestrator.run();

          // Phase 4: summarize results
          let finalResult = '';
          const allResults = orchestrator.getResults();
          if (steps.length === 1) {
            finalResult = allResults[0]?.output || '任务已执行';
          } else {
            const completed = allResults.filter(r => r.status === 'done');
            if (completed.length === allResults.length) {
              const lastStep = steps[steps.length - 1];
              const lastResult = allResults.find(r => r.id === lastStep.id);
              finalResult = lastResult?.output || '所有步骤已完成';
            } else {
              const failed = allResults.filter(r => r.status === 'failed');
              const skipped = allResults.filter(r => r.status === 'skipped');
              finalResult = "任务部分完成。成功 " + completed.length + "/" + allResults.length + "，失败 " + failed.length + "，跳过 " + skipped.length + "。";
              if (failed.length > 0) {
                finalResult += "\n失败步骤：" + failed.map(f => f.id + "(" + (f.error || "") + ")").join(", ");
              }
            }
          }

          // update plan status
          const p = activeTaskPlans.get(taskMsgId);
          if (p) {
            p.status = orchResult.success ? 'completed' : 'failed';
            p.result = finalResult;