// 重构前 index.ts 只读参考：commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f，blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58；可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。

import type {
  CodexRunResult,
  FeedbackPromptRecord,
  JudgeDecision,
  JudgeFailureDetails,
  JudgeProbeCase,
  JudgeResult,
  JsonValue,
  LiveOutput,
  MiniMaxJudgeResponse,
  ParsedJudgeJson,
  QueueTask,
} from "./types";

export interface JudgeRuntimeContext {
  config: {
    minimaxApiKey: string;
    minimaxApiBase: string;
    minimaxModel: string;
    judgeTimeoutMs: number;
    judgeRepairAttempts: number;
    judgeMaxTokens: number;
  };
  logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void;
  safePreview: (value: string, max?: number) => string;
  userPromptForDisplay: (prompt: string) => string;
  taskFullOutput: (task: QueueTask) => LiveOutput[];
  taskReferenceIds: (task: QueueTask) => string[];
  extractRecord: (value: unknown) => Record<string, unknown> | null;
  extractString: (value: unknown, key: string) => string | null;
  promptLineCount: (text: string) => number;
  judgeFailRetryLimit: number;
  publishJudgeEvent?: (task: QueueTask, type: string, payload: Record<string, JsonValue>) => void;
}

let context: JudgeRuntimeContext | null = null;

export function configureJudge(runtimeContext: JudgeRuntimeContext): void {
  context = runtimeContext;
}

function ctx(): JudgeRuntimeContext {
  if (context === null) throw new Error("judge module is not configured");
  return context;
}

function config(): JudgeRuntimeContext["config"] {
  return ctx().config;
}

function logger(level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue): void {
  ctx().logger(level, message, data);
}

function publishJudgeEvent(task: QueueTask, type: string, payload: Record<string, JsonValue>): void {
  const publisher = ctx().publishJudgeEvent;
  if (publisher === undefined) return;
  try {
    publisher(task, type, payload);
  } catch (error) {
    logger("warn", "judge_oa_event_publish_failed", { taskId: task.id, type, error: error instanceof Error ? error.message : String(error) });
  }
}

function safePreview(value: string, max?: number): string {
  return ctx().safePreview(value, max);
}

function userPromptForDisplay(prompt: string): string {
  return ctx().userPromptForDisplay(prompt);
}

function taskFullOutput(task: QueueTask): LiveOutput[] {
  return ctx().taskFullOutput(task);
}

function taskReferenceIds(task: QueueTask): string[] {
  return ctx().taskReferenceIds(task);
}

function extractRecord(value: unknown): Record<string, unknown> | null {
  return ctx().extractRecord(value);
}

function extractString(value: unknown, key: string): string | null {
  return ctx().extractString(value, key);
}

function promptLineCount(text: string): number {
  return ctx().promptLineCount(text);
}

function failureDetailReason(detail: JudgeFailureDetails): string {
  const timeout = detail.timedOut ? `timed out after ${detail.timeoutMs}ms` : detail.errorMessage;
  const promptSize = detail.promptChars === undefined ? "" : `，prompt=${detail.promptChars} chars/${detail.promptLines ?? "?"} lines`;
  return `MiniMax judge ${timeout}${promptSize}`;
}

function fallbackJudge(result: CodexRunResult, minimaxError?: string | JudgeFailureDetails): JudgeResult {
  if (result.transportClosedBeforeTerminal || result.terminalStatus === null) {
    return { decision: "retry", confidence: 0.75, reason: "Codex app-server 在 turn/completed 之前关闭。", continuePrompt: retryInstruction, source: "fallback" };
  }
  if (result.terminalStatus === "failed") {
    return { decision: "retry", confidence: 0.7, reason: result.terminalError ? `Codex turn 失败：${result.terminalError}` : "Codex turn 失败。", continuePrompt: retryInstruction, source: "fallback" };
  }
  if (result.terminalStatus === "interrupted") {
    return { decision: "fail", confidence: 0.8, reason: "Codex turn 被用户请求打断。", source: "fallback" };
  }
  if (result.finalResponse.trim().length === 0) {
    return { decision: "retry", confidence: 0.78, reason: "Codex turn 没有返回最终 assistant response，不能视为任务已完成。", continuePrompt: retryInstruction, source: "fallback" };
  }
  if (minimaxError !== undefined) {
    const failureDetails = typeof minimaxError === "string" ? null : minimaxError;
    const errorText = typeof minimaxError === "string" ? minimaxError : failureDetailReason(minimaxError);
    return {
      decision: "retry",
      confidence: 0.65,
      reason: `MiniMax judge 失败（${safePreview(errorText, 300)}）；安全 fallback 将继续现有 session，而不是把 turn/completed 当作任务已完成。`,
      source: "fallback",
      failureDetails,
      raw: failureDetails === null
        ? { minimaxFailure: { errorMessage: String(minimaxError) } }
        : { minimaxFailure: failureDetails as unknown as JsonValue },
    };
  }
  return { decision: "complete", confidence: 0.65, reason: "Codex 输出了 completed 状态的 turn/completed，且未配置 MiniMax judge。", source: "fallback" };
}

export const retryInstruction = "这是同一个 Codex thread 的 continuation，不是新任务。请基于上文继续完成原始任务；只做最小必要状态核查，避免从头重新摸索、避免重复已经完成的修改。";
const codeQueueD601DeployCommand = "bun scripts/cli.ts ssh D601 \"nohup bash -lc 'cd ~/cq-deploy && CODE_QUEUE_ENV_FILE=.state/code-queue-d601.env docker compose -f src/components/microservices/code-queue/docker-compose.d601.yml up -d --build --force-recreate code-queue > /tmp/unidesk-code-queue-redeploy.log 2>&1' >/tmp/unidesk-code-queue-redeploy-launch.log 2>&1 & echo scheduled:/tmp/unidesk-code-queue-redeploy.log\"";
const codeQueueRestartSafetyGuidance = `Code Queue 服务具备 restart-recovery，允许在任何时候重启、重建或替换 D601 的 \`code-queue-backend\`；当当前任务修改 Code Queue 自身时，禁止等待当前 Code Queue task 退出或等待队列归零后再重启，因为这会等待自己退出形成自锁。正确做法是通过 D601 provider SSH 在宿主侧 detached 调度 \`${codeQueueD601DeployCommand}\` 或等价 detached no-deps force-recreate；禁止从正在运行的 \`code-queue-backend\` 容器里以前台方式执行会重建自身的 compose 命令，否则发起命令的容器被替换时可能留下 Created/Exited 的半完成状态。调度后在恢复的 live health/task 查询中验证。`;

export function explicitUserInterrupt(task: QueueTask, result: CodexRunResult): boolean {
  return result.terminalStatus === "interrupted"
    && (task.cancelRequested || task.output.some((item) => item.method === "turn/interrupt" || item.text.includes("interrupt requested")));
}

function currentAttemptOutputForJudge(task: QueueTask): LiveOutput[] {
  const latestAttempt = task.attempts.at(-1);
  const startSeq = Number(latestAttempt?.outputStartSeq ?? NaN);
  const endSeq = Number(latestAttempt?.outputEndSeq ?? NaN);
  const output = taskFullOutput(task);
  if (Number.isFinite(startSeq)) {
    return output.filter((item) => item.seq >= startSeq && (!Number.isFinite(endSeq) || item.seq <= endSeq));
  }
  return task.output.slice(-80);
}

function judgeEvidenceText(task: QueueTask, result: CodexRunResult): string {
  const currentOutput = currentAttemptOutputForJudge(task);
  return [
    result.terminalError ?? "",
    result.appServerExit.stderrTail,
    ...currentOutput.slice(-120)
      .filter((item) => item.channel === "error" || item.method === "turn/completed" || item.method === "app-server" || item.method?.includes("watchdog") === true)
      .map((item) => item.text),
    ...result.events.slice(-80).map((event) => [event.method, event.status, event.message, event.textPreview].filter(Boolean).join(" ")),
  ].join("\n");
}

function encodedBytes(value: string): number {
  return new TextEncoder().encode(value).length;
}

function compactJudgeOutputItems(output: LiveOutput[]): { items: Array<{ channel: string; text: string; method?: string; seq?: number; at?: string }>; omitted: number; totalTextChars: number } {
  const important = output.filter((item) => {
    const method = String(item.method || "");
    const text = String(item.text || "");
    return item.channel === "error"
      || method === "turn/completed"
      || method === "item/completed"
      || method === "item/started"
      || method === "provider/container"
      || method === "execution/mode"
      || method.includes("watchdog")
      || /(error|failed|失败|timeout|timed out|aborted|rebuild|health|succeeded|verified|passed|judge)/iu.test(text);
  });
  const bySeq = new Map<number, LiveOutput>();
  for (const item of [...important.slice(-32), ...output.slice(-28)]) {
    const seq = Number(item.seq);
    if (Number.isFinite(seq)) bySeq.set(seq, item);
  }
  const selected = Array.from(bySeq.values()).sort((left, right) => Number(left.seq) - Number(right.seq));
  const items: Array<{ channel: string; text: string; method?: string; seq?: number; at?: string }> = [];
  let remainingChars = 14_000;
  let totalTextChars = 0;
  for (const item of selected) {
    const rawText = String(item.text || "");
    totalTextChars += rawText.length;
    if (remainingChars <= 0) break;
    const perItem = item.channel === "error" ? 700 : 360;
    const max = Math.max(120, Math.min(perItem, remainingChars));
    const text = safePreview(rawText, max);
    remainingChars -= text.length;
    items.push({
      channel: item.channel,
      method: item.method,
      seq: item.seq,
      at: item.at,
      text,
    });
  }
  return { items, omitted: Math.max(0, output.length - items.length), totalTextChars };
}

function compactJudgeEvents(events: CodexRunResult["events"]): { events: CodexRunResult["events"]; omitted: number } {
  const selected = events.slice(-30).map((event) => ({
    ...event,
    message: event.message === undefined ? undefined : safePreview(event.message, 300),
    textPreview: event.textPreview === undefined ? undefined : safePreview(event.textPreview, 300),
  }));
  return { events: selected, omitted: Math.max(0, events.length - selected.length) };
}

function hasServiceLimitOrTransportError(text: string): boolean {
  return /(429|Too Many Requests|rate limit|quota exceeded|overloaded|exceeded retry limit|stream disconnected|no activity timeout|app-server closed|ECONNRESET|ETIMEDOUT)/iu.test(text);
}

function compactLatestAttemptForJudge(attempt: QueueTask["attempts"][number] | null): Record<string, unknown> | null {
  if (attempt === null) return null;
  return {
    index: attempt.index,
    mode: attempt.mode,
    startedAt: attempt.startedAt,
    finishedAt: attempt.finishedAt,
    providerId: attempt.providerId,
    executionMode: attempt.executionMode,
    terminalStatus: attempt.terminalStatus,
    transportClosedBeforeTerminal: attempt.transportClosedBeforeTerminal,
    appServerExitCode: attempt.appServerExitCode,
    appServerSignal: attempt.appServerSignal,
    error: attempt.error,
    finalResponsePreview: safePreview(String(attempt.finalResponsePreview || attempt.finalResponse || ""), 1200),
    finalResponseChars: Number(attempt.finalResponseChars ?? String(attempt.finalResponse || "").length),
    stderrTail: safePreview(String(attempt.stderrTail || ""), 1200),
    outputStartSeq: attempt.outputStartSeq ?? null,
    outputEndSeq: attempt.outputEndSeq ?? null,
  };
}

function judgePrompt(task: QueueTask, result: CodexRunResult): string {
  const latestAttempt = task.attempts[task.attempts.length - 1] ?? null;
  const originalUserTask = task.basePrompt || userPromptForDisplay(task.prompt);
  const resolvedPromptForCodex = task.prompt === originalUserTask ? null : safePreview(task.prompt, 6000);
  const currentAttemptOutput = currentAttemptOutputForJudge(task);
  const outputEvidence = compactJudgeOutputItems(currentAttemptOutput);
  const eventEvidence = compactJudgeEvents(result.events);
  // Keep this record factual. Do not add local completion gates here; MiniMax
  // is authoritative whenever it returns a valid judge JSON.
  return JSON.stringify({
    instruction: "请判定一个 Codex 编码任务是否真正完成、是否应通过向现有 Codex thread 追加 continuation prompt 继续重试，或是否应作为不可重试失败处理。只能返回 JSON，不要输出 Markdown fence、解释性正文或注释。所有自然语言字段必须使用中文，尤其是 reason 和 continuePrompt。重要：普通的 Codex turn/completed 状态只表示传输/session 终止事件，不等于用户任务已完成。决策前必须检查当前尝试的 transcript、最终回复、命令/文件变更事件、stderr 和原始任务。请严格判定：如果用户任务中的任一显式验收项缺少已完成证据，就选择 retry。最常见错误是把未完成或跑偏的工作标成 fail；未完成工作必须选择 retry，让同一个 session 继续。不要把更早 attempt 的限流/中断证据自动当作当前 attempt 的完成门禁；如果当前最新 attempt 已经提供完整完成证据，可以判定 complete。",
    schema: { decision: "complete|retry|fail", confidence: "0..1", reason: "中文短句", continuePrompt: "decision=retry 时必填，除非确实没有可用的继续提示；内容必须是中文，保持简洁，不要粘贴原始任务、引用上下文、transcript 或 JSON" },
    originalTask: safePreview(originalUserTask, 6000),
    resolvedPromptForCodex,
    attempt: task.currentAttempt,
    maxAttempts: task.maxAttempts,
    executionRecord: {
      terminalStatus: result.terminalStatus,
      terminalError: result.terminalError,
      transportClosedBeforeTerminal: result.transportClosedBeforeTerminal,
      appServerExitCode: result.appServerExit.code,
      appServerSignal: result.appServerExit.signal,
      stderrTail: safePreview(result.appServerExit.stderrTail, 2000),
      finalResponse: safePreview(result.finalResponse, 6000),
      finalResponseChars: result.finalResponse.length,
      finalResponseMissing: result.finalResponse.trim().length === 0,
      latestAttempt: compactLatestAttemptForJudge(latestAttempt),
      judgeFailCount: task.judgeFailCount,
      judgeFailRetryLimit: ctx().judgeFailRetryLimit,
      cancelRequested: task.cancelRequested,
      currentAttemptOutput: outputEvidence.items,
      currentAttemptOutputOmitted: outputEvidence.omitted,
      currentAttemptOutputTotalTextChars: outputEvidence.totalTextChars,
      currentAttemptEvents: eventEvidence.events,
      currentAttemptEventsOmitted: eventEvidence.omitted,
    },
    policy: {
      complete: "仅当 transcript/最终回复证明当前任务确实完成，并且每个显式验收项都有证据时使用；队列 worker 随后会推进到下一个 queued 任务。",
      retry: "当当前任务未完成、Codex 只做了计划、跳过了请求的编辑/命令、只完成了部分工作、在 steer prompt 后跑偏到旁支任务、需要再跑一轮，或遇到临时网络/server/disconnected/transport/internal 错误时使用。只要存在 thread id，retry 必须恢复现有 thread 并追加 continuePrompt。",
      fail: "仅用于明确的用户打断/取消、缺少 agent 无法补充的凭据/权限/用户输入，或已证实的确定性不可重试外部阻塞。不要仅因为 agent 漏掉核心目标、产出错误/部分工作、或未运行必要验证就使用 fail；这些都应选择 retry。",
      codeQueueSelfRestart: codeQueueRestartSafetyGuidance,
    },
    retryContinuePromptRules: [
      "当 decision=retry 时，continuePrompt 是给同一个 Codex thread 的反馈；只能基于 originalTask 中已存在的要求和 executionRecord 中已证实的缺口，不得新增 originalTask 没有要求的 API 形态、参数签名、实现细节、文件路径或命令细节。",
      "continuePrompt 只写“未完成哪些原始需求，继续按照原始需求补齐剩余任务”和必要验收证据，通常不超过 1200 字；不要复制 originalTask、resolvedPromptForCodex、recentOutput、引用任务全文或完整 transcript。",
      "如果 retry 原因是 429/Too Many Requests/exceeded retry limit、transport/app-server 中断、finalResponse 为空，或 executionRecord 还没有足够证据确认具体未完成细节，continuePrompt 必须保持高层反馈：`未完成xxx原始需求，继续按照原始需求完成剩余任务`；不要推测下一步代码写法。",
      "如果缺口很多，必须在生成 continuePrompt 的源头去重、合并和分组；不要依赖接收方对已生成长文本做末端截断，因为截断会丢失验收信息。",
      "列出缺失的验收证据，并要求下一轮在最终回复中给出真实命令/API/UI 结果。",
      "如果 agent 在交付中途停下来询问用户如何处理一个它本不需要触碰的并发修改文件，continuePrompt 必须要求它忽略该并发文件并继续交付自己的变更范围，而不是让用户选择。",
      "对于涉及运行中的服务或 WebUI 的任务，如果原始验收项明确要求运行态结果，continuePrompt 可以提示补充相应验证；不要把部署本身当作所有任务的统一门槛。",
      `如果原始任务明确要求运行中的服务或已服务 UI 证据，continuePrompt 可以点名 D601 Code Queue 重建命令 \`${codeQueueD601DeployCommand}\` 和 \`bun scripts/cli.ts server rebuild frontend\`，并按场景要求 live API/WebUI 验证，例如 \`/api/judge/probe\`、\`/trace-summary\` 或已服务的 Code Queue 页面。`,
      "如果受影响服务包含 Code Queue 自身，continuePrompt 禁止要求等待当前 task 结束、等待 queue idle 或等待 `0 running` 后再重启；这会形成自锁。应明确 Code Queue 可以立即重启/重建，并依赖 restart-recovery 恢复本任务后继续验证。",
    ],
    hallucinationNoiseGuardrails: [
      "judge 只能判断完成/未完成和指出原始需求缺口，不能充当实现规划器；不要在 feedback 中发明“下一步必须这样改”的新要求。",
      "如果 originalTask 只要求分析初始化/配置并在既有打印条件下新增滤波前/后打印，continuePrompt 不得额外要求把现有 API 改成无参数调用，也不得指定函数签名从 out-parameter 改成 return value。",
      "若缺口是“原始任务尚未完成”，feedback 采用高层模板：未完成<原始需求摘要>，继续按照原始需求完成剩余任务，并在最终回复给出证据。",
    ],
    completionEvidenceGuidance: [
      "如果原始任务明确要求运行中的服务或已服务 UI 结果，而 finalResponse 表示“我没有重建运行中的容器”“后续如需上线”“若要上线验证”“未上线”“not deployed”“not rebuilt”或等价含义，则可以选择 retry；否则不要把“未部署”本身当作统一门槛。",
      "如果 transcript 只证明源码编辑、检查或构建，但原始任务明确要求 runtime/UI/service 变更的部署后 live API/browser 验证，则选择 retry；如果原始任务没有这类要求，则不要仅因未部署就自动判定未完成。",
      "把 rebuild/deploy 描述为可选、建议、未来工作或下一步，只在原始任务明确要求运行态结果时才构成未完成证据。",
      "判断 429、Too Many Requests、exceeded retry limit、stream disconnected 等限流/传输证据时，只把当前最新 attempt 的证据作为当前判定依据；更早 attempt 的失败不能自动否定后续正常完成的 attempt。",
      "如果当前 attempt 的 terminalStatus 是 failed/null/interrupted，transportClosedBeforeTerminal 为 true，或当前 attempt 以 Codex/API 基础设施错误结束，则禁止 complete；除非这是应判为 fail 的明确用户打断。service/rate-limit/transport/internal 失败应选择 retry。",
      "如果当前 attempt 的 stderr、terminalError、currentAttemptOutput 或 currentAttemptEvents 包含 429、Too Many Requests、rate limit、quota、overloaded、exceeded retry limit、stream disconnected、no activity timeout 或 app-server closed，即使错误前发生过代码编辑或重建，也要选择 retry。",
      "如果 terminalStatus=failed 且 finalResponse 为空，必须选择 retry；即使中途已有容器 healthy、API 200、目录列表、文件改动或文档更新证据，也不能把没有最终回复的失败 turn 判为 complete。",
      "如果原始任务要求运行、通过、复现、比较、打分、benchmark、validate 或证明经验结果，complete 必须有 transcript 证据显示请求的命令/pipeline/scorer 已运行，并给出结果数字或状态。",
      "如果任务要求 A/B、ablation、with/without、before/after、skill/no-skill、baseline/optimized 或正/负样本比较，complete 必须为每个请求侧都提供证据；只实现一侧代码或测试属于未完成。",
      "如果最终回复说必需的 benchmark/pipeline 未运行、为节省 quota/time 跳过、应作为下一步运行，或仅根据代码检查预期会通过，即使单元测试通过，也要选择 retry。",
      "不要接受用 unit/type/component 测试替代明确请求的 benchmark 分数或 pipeline 运行的自我辩解。",
      "对于 frontend 或 WebUI 可见变更，源码编辑和 type check 不够。complete 需要证明已重建/重启或刷新已服务的 frontend bundle；当用户可见行为是验收目标时，还要有针对运行中 UniDesk frontend 的 browser/E2E/UI 验证。",
      "如果 frontend/UI 任务的最终回复说 public/full E2E 未运行，或 transcript 缺少 frontend rebuild/server rebuild 加 served-UI 验证，而请求变更可能在已部署 UI bundle 中不可见，则选择 retry。",
      "对于 Code Queue、backend-core、provider-gateway、frontend 或任何其他 UniDesk service/runtime 行为变更，源码编辑、TypeScript 检查和本地构建不够。complete 需要证明每个受影响的运行中 service/container/bundle 已重建或重启，并且部署后的 live API/UI 行为已验证。",
      "如果最终回复同时给出已执行的容器 recreate/restart、运行中 service/image/started/healthy 证据和 live API/UI 验证，且只把 Docker mirror/build 可复现性、后续常规重建或 registry 修复作为建议，不要仅因出现“后续重建/建议修 mirror”等字样改判 retry。",
      "对于 Code Queue 自身的 runtime 行为变更，不得把“等待当前 Code Queue task 结束/等待自己退出后再重启”当作完成计划或阻塞理由；这是自锁。应选择 retry，反馈要求直接重启/重建 Code Queue 并在 restart-recovery 后验证 live health/task 证据。",
      "如果用户要求功能在 WebUI 可见或“上线/生效/提供展示”，不要把 rebuild/restart/deploy 当成建议。若没有运行中服务或已服务浏览器 UI 的证据，选择 retry，并要求部署加验证。",
      "如果最终回复说它停下来要求用户确认如何处理一个 unexpected/concurrently modified 文件，而该文件不在 agent 必要交付范围内，则选择 retry：任务尚未自主交付完成。",
      "对于提到 4/4、8/8、40/40 或 skill/no-skill 行为等分数的 PikaPython/PikaBench 任务，在 complete 前必须有实际 scorer 或 pipeline 运行证据，证明请求分数和比较结果。",
      "对于 hardware、firmware、provider、WSL、SSH passthrough、skill、compile、flash/download、serial 或 board-comm 任务，complete 需要请求的操作步骤证据；如果请求了 download/board/serial 验证但没有证据，只有部分文档或一次成功 build 应选择 retry。",
      "如果最终回复主要处理后来的 steer prompt，而原始任务仍有部分未完成，应选择 retry，并给出同时协调 steer 与原始任务的 continuation prompt。",
    ],
    classicFailureExamples: [
      {
        pattern: "原始任务要求：没有通用 PikaPython benchmark skill 时不能达到 4/4；有该 skill 时必须达到 4/4。",
        incompleteEvidence: "执行代理只是把定向 prompt 内容移入 skill，运行了 type/component/unit 测试，并表示为节省 quota 没有启动 PikaBench-4 baseline/no-skill 实跑。",
        requiredDecision: "retry",
        reason: "请求的 with-skill/no-skill 经验 pipeline 对照没有运行，因此声明的验收条件没有被证明。",
      },
      {
        pattern: "原始任务要求学习 D601 ConStart/constar 固件工作区、使用 skill 完成 compile/download 等，并更新长期文档；后续 steer 要求把项目文档移动到 constar/docs。",
        incompleteEvidence: "执行代理做了一些 skill discovery 和文档迁移，但最终回复主要围绕 steer 驱动的文档迁移，未证明所有请求的 compile/download/serial/board-comm 操作验收点已完成。",
        requiredDecision: "retry",
        reason: "这是原始任务未完成，不是不可重试失败；应继续同一个 session，补齐缺失的操作验证和文档。",
      },
      {
        pattern: "原始任务要求停止 ClaudeQQ 登录二维码自动刷新。代码已编辑且 frontend rebuild 成功，但 Codex turn 后来在 E2E 或依赖验证仍在运行时因 429 Too Many Requests / exceeded retry limit 失败结束。",
        incompleteEvidence: "终止 turn 状态是 failed，最终回复为空或缺少完整总结，且验证因服务限流没有完成。",
        requiredDecision: "retry",
        reason: "rate-limit 或 retry-limit 终止属于基础设施中断，不能算完成，必须继续现有 session。",
      },
      {
        pattern: "原始任务要求基于 filebrowser 开发/部署文件管理器，支持 main server host、provider host 和 WSL Windows /mnt/c 文件浏览，例如 task codex_1778545138372_ec5e05。",
        incompleteEvidence: "中途命令显示 main-server/D601/D518 filebrowser 容器健康、WebUI/API/Windows 路径一度可读，但最后出现 `exceeded retry limit, last status: 429 Too Many Requests`、turn completed status=failed，且 finalResponse 为空。",
        requiredDecision: "retry",
        reason: "当前 attempt 的限流失败和空 finalResponse 是未完成证据；不能用中途运行证据替代失败 turn 的最终交付。必须继续同一 thread 生成最终总结并补齐/复核验证。",
      },
      {
        pattern: "原始任务要求把 UniDesk sidebar/WebUI 标签从“微服务”改为“用户服务”，并更新长期文档。",
        incompleteEvidence: "执行代理编辑了 frontend 源码、文档和 E2E selector，并运行 `bun scripts/cli.ts check`，但没有重建/重启 frontend bundle，也没有验证运行中的 served UI；最终回复还说 full public E2E 未运行。用户后来观察到 UI 变更实际上未生效。",
        requiredDecision: "retry",
        reason: "WebUI 可见变更只有在 deployed/served frontend 已重建或刷新并验证后才算完成；源码编辑加 type check 可能让 live UI 保持不变。",
      },
      {
        pattern: "原始任务要求移除 Code Queue 前端的 `Prompt 全量` card，因为 TraceView 已可展开它，例如 task codex_1778483956252_c65680。",
        incompleteEvidence: "执行代理编辑 frontend 并运行检查，但最终回复停下来要求用户选择如何处理 `src/components/microservices/code-queue/src/index.ts`；它自己说没有修改该文件，并给出忽略该文件、只交付 frontend 变更的选项。",
        requiredDecision: "retry",
        requiredContinuePrompt: "这是其他任务正在并发开发，忽略这个文件，只继续交付我已改的前端相关变更。",
        reason: "agent 尚未自主完成交付；即使安全下一步是忽略无关并发文件并完成自己的 frontend 范围，它仍暂停要求用户确认。",
      },
      {
        pattern: "原始任务要求 Code Queue 在 trace 链路和 WebUI 中暴露 judge feedback prompt，例如 task codex_1778476426776_a2bf19。",
        incompleteEvidence: "执行代理编辑 backend/frontend 源码并通过 type check，但没有重建 D601 `code-queue-backend`，没有 `server rebuild frontend` 或等价 served bundle 部署，也没有 live API/UI 验证证明 `/trace-summary` 或 WebUI 包含 judge feedback prompt。最终回复说“我没有重建运行中的容器”，或把 rebuild/deploy 当作建议/未来工作。",
        requiredDecision: "retry",
        requiredContinuePrompt: "请让同一个 Codex thread 部署已修改的 Code Queue/frontend 服务，然后在声明完成前验证 live API/UI 证据。",
        reason: "这是 service/UI 功能。只有运行中的 Code Queue backend 和 served frontend 已更新并验证后才算完成；否则用户仍看不到该功能。",
      },
      {
        pattern: "原始任务要求修改或验证 Code Queue 自身，并且完成条件需要重启/重建 `code-queue-backend`。",
        incompleteEvidence: "执行代理表示要等当前 Code Queue task 结束、等队列空闲、等 0 running、或等自己退出后再重启 Code Queue，因此没有执行重启/重建和恢复后 live 验证。",
        requiredDecision: "retry",
        requiredContinuePrompt: `不要等待当前 Code Queue task 退出；D601 Code Queue 可随时重启/重建。请直接执行 \`${codeQueueD601DeployCommand}\` 或等价 no-deps force-recreate，依赖 restart-recovery 恢复本任务，然后验证 live health/task 证据。`,
        reason: "等待当前任务结束后再重启 Code Queue 会让任务等待自己退出，属于自锁；正确交付路径是先重启/重建再由 restart-recovery 继续。",
      },
    ],
  });
}

function parseRecordJson(text: string, source: string): ParsedJudgeJson {
  const parsed = JSON.parse(text) as unknown;
  if (typeof parsed === "string") return parseJudgeJson(parsed);
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${source} parsed to non-object JSON`);
  return { value: parsed as Record<string, unknown>, source };
}

function balancedJsonCandidates(text: string): string[] {
  const candidates: string[] = [];
  for (let start = 0; start < text.length; start += 1) {
    if (text[start] !== "{") continue;
    let depth = 0;
    let inString = false;
    let escaped = false;
    for (let index = start; index < text.length; index += 1) {
      const char = text[index] ?? "";
      if (inString) {
        if (escaped) {
          escaped = false;
        } else if (char === "\\") {
          escaped = true;
        } else if (char === "\"") {
          inString = false;
        }
        continue;
      }
      if (char === "\"") {
        inString = true;
      } else if (char === "{") {
        depth += 1;
      } else if (char === "}") {
        depth -= 1;
        if (depth === 0) {
          candidates.push(text.slice(start, index + 1));
          break;
        }
      }
    }
  }
  return candidates;
}

function judgeJsonCandidates(text: string): Array<{ source: string; text: string }> {
  const normalized = text.replace(/^\uFEFF/u, "").trim();
  const candidates: Array<{ source: string; text: string }> = [];
  const pushText = (source: string, value: string): void => {
    const trimmed = value.trim();
    if (trimmed.length > 0) candidates.push({ source, text: trimmed });
  };
  const pushDerivedCandidates = (sourcePrefix: string, value: string): void => {
    const trimmed = value.trim();
    if (trimmed.length === 0) return;
    pushText(sourcePrefix, trimmed);
    const fenced = /```[ \t]*(?:json|JSON|javascript|js)?[^\n\r]*[\r\n]+([\s\S]*?)```/gu;
    for (const match of trimmed.matchAll(fenced)) {
      const body = typeof match[1] === "string" ? match[1].trim() : "";
      if (body.length > 0) pushText(`${sourcePrefix}_fenced`, body);
    }
    const strippedFence = trimmed
      .replace(/^```[^\n\r]*[\r\n]?/u, "")
      .replace(/```$/u, "")
      .trim();
    if (strippedFence !== trimmed) pushText(`${sourcePrefix}_stripped_fence`, strippedFence);
    const strippedLabel = trimmed.replace(/^(?:json|JSON)\s*[:\n\r]\s*/u, "").trim();
    if (strippedLabel !== trimmed) pushText(`${sourcePrefix}_stripped_label`, strippedLabel);
    const jsonTag = trimmed.match(/<json\b[^>]*>([\s\S]*?)<\/json>/iu);
    if (typeof jsonTag?.[1] === "string") pushText(`${sourcePrefix}_json_tag`, jsonTag[1]);
    for (const candidate of balancedJsonCandidates(trimmed)) pushText(`${sourcePrefix}_balanced_object`, candidate);
  };
  pushDerivedCandidates("direct", normalized);
  const withoutClosedThink = normalized.replace(/<(?:think|thinking)\b[^>]*>[\s\S]*?<\/(?:think|thinking)>/giu, "").trim();
  if (withoutClosedThink !== normalized) pushDerivedCandidates("stripped_think", withoutClosedThink);
  const closeThinkMatches = Array.from(normalized.matchAll(/<\/(?:think|thinking)>/giu));
  const lastThinkClose = closeThinkMatches.at(-1);
  if (lastThinkClose?.index !== undefined) {
    const afterThink = normalized.slice(lastThinkClose.index + lastThinkClose[0].length).trim();
    if (afterThink.length > 0) pushDerivedCandidates("after_think", afterThink);
  }
  const seen = new Set<string>();
  return candidates.filter((candidate) => {
    const key = candidate.text;
    if (key.length === 0 || seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

export function parseJudgeJson(text: string): ParsedJudgeJson {
  let lastError = "no candidate JSON was found";
  for (const candidate of judgeJsonCandidates(text)) {
    try {
      const parsed = parseRecordJson(candidate.text, candidate.source);
      if (validJudgeDecisionValue(parsed.value.decision)) return parsed;
      lastError = `${candidate.source} parsed JSON does not contain a valid decision`;
    } catch (error) {
      lastError = error instanceof Error ? error.message : String(error);
    }
  }
  throw new Error(`MiniMax judge did not return parseable JSON after denoise: ${lastError}; preview=${safePreview(text, 500)}`);
}

function normalizedDecision(value: unknown): JudgeDecision {
  if (value === "complete" || value === "retry" || value === "fail") return value;
  if (value === "continue") return "retry";
  return "retry";
}

function validJudgeDecisionValue(value: unknown): boolean {
  return value === "complete" || value === "retry" || value === "fail" || value === "continue";
}

const retryTaskSummaryMaxChars = 1200;
const judgeReasonPromptMaxChars = 1200;
export const compactContinuationPromptTargetChars = 1200;
export const continuePromptSourceBudgetChars = 4000;

export function judgeReasonForPrompt(reason: string): string {
  return safePreview(reason, judgeReasonPromptMaxChars) || "(empty)";
}

function continuationPromptForRetry(prompt: string): string {
  return prompt.trim();
}

export function compactRetryTaskContext(task: QueueTask): string {
  const basePrompt = task.basePrompt || userPromptForDisplay(task.prompt);
  const referenceTaskIds = taskReferenceIds(task);
  return [
    "原始任务摘要（同一 thread 已有完整上文，不重新注入引用全文）：",
    safePreview(basePrompt, retryTaskSummaryMaxChars) || "(empty)",
    referenceTaskIds.length > 0 ? `引用任务 ID：${referenceTaskIds.join(", ")}` : "",
    `按需查询有界摘要：bun scripts/cli.ts codex task ${task.id}`,
  ].filter((line) => line.length > 0).join("\n");
}

export function queueRecoveryRetryPrompt(task: QueueTask, reason: string): string {
  return [
    retryInstruction,
    "Code Queue 服务在任务运行中重启/停止；这是自动恢复提示，不是新任务，也不需要重新粘贴原始任务或引用全文。",
    "如果本轮任务正是修改 Code Queue 自身，不要等待当前 task 退出；服务重启已经发生，继续完成恢复后的验证和剩余交付。",
    `恢复原因：${judgeReasonForPrompt(reason)}`,
    "请基于当前 thread 上文继续，只做最小必要状态核查，恢复未完成的等待、验证、部署或命令；最终 response 必须给出真实结果证据。",
    "原始任务摘要/按需查询：",
    compactRetryTaskContext(task),
  ].join("\n\n");
}

export function retryPrompt(task: QueueTask, judge: JudgeResult): string {
  if (judge.continuePrompt !== undefined && judge.continuePrompt.trim().length > 0) return continuationPromptForRetry(judge.continuePrompt);
  return [
    retryInstruction,
    "上一次 judge 判定为 retry，下面是必须传递给本轮 continuation 的 judge feedback。请优先补齐这些缺口，不要只做泛泛的状态核查。",
    `judge 来源：${judge.source}`,
    `judge 置信度：${judge.confidence.toFixed(2)}`,
    `judge 未完成原因：${judgeReasonForPrompt(judge.reason)}`,
    "请在本轮最终 response 中明确说明已如何解决上述 judge feedback；如果 judge 要求 benchmark/上线/运行中验证，就必须提供对应真实命令和结果证据。",
    "原始任务摘要/按需查询：",
    compactRetryTaskContext(task),
  ].join("\n\n");
}

export function judgeFailContinuationPrompt(task: QueueTask, judge: JudgeResult, failCount: number): string {
  const limit = ctx().judgeFailRetryLimit;
  const parts = [
    `上一次 judge 判定为 fail (${failCount}/${limit})，但 Code Queue 策略要求：非用户取消、非确定不可恢复的情况必须当作“未完成”继续当前 session，直到 fail 累计 ${limit} 次才真正放弃。`,
    "这是同一个 Codex thread 的 continuation，不是新任务；不要重新开局或从头摸索，只补齐缺失验收项。",
    `judge 理由：${judgeReasonForPrompt(judge.reason)}`,
    "请不要放弃或新开任务。继续完成原始任务中尚未完成/未验证的验收项，优先补齐 judge 指出的缺口，并给出真实命令、文件或运行结果证据。",
  ];
  if (judge.continuePrompt !== undefined && judge.continuePrompt.trim().length > 0) {
    parts.push("judge 建议的继续提示：", continuationPromptForRetry(judge.continuePrompt));
  }
  parts.push("原始任务摘要/按需查询：", compactRetryTaskContext(task));
  return parts.join("\n\n");
}

export function parsedContinuePromptForJudge(parsed: Record<string, unknown>, decision: JudgeDecision): string | undefined {
  const prompt = typeof parsed.continuePrompt === "string" ? parsed.continuePrompt.trim() : "";
  if (prompt.length === 0 || decision === "complete") return undefined;
  if (prompt.length > continuePromptSourceBudgetChars) {
    throw new Error(`MiniMax judge continuePrompt exceeds source budget (${prompt.length}/${continuePromptSourceBudgetChars} chars); resynthesize compact feedback at the source instead of tail-truncating`);
  }
  return prompt;
}

const judgeSystemPrompt = [
  "你是严格的任务状态分类器。",
  "你没有工具访问，不能执行命令、读取文件或续写编码 agent 行为；只能根据 user 消息里的 JSON evidence 判定。",
  "只能返回一个紧凑原始 JSON object。禁止输出 Markdown fence、<think>、思考过程、解释性正文或注释。",
  "所有自然语言字段必须使用中文，尤其是 reason 和 continuePrompt。",
].join("");

const judgeResponseSchema = {
  decision: "complete|retry|fail",
  confidence: "0..1",
  reason: "中文短句",
  continuePrompt: `decision=retry 时必填，除非确实没有可用的继续提示；内容必须是中文，目标不超过 ${compactContinuationPromptTargetChars} 字，绝对不超过 ${continuePromptSourceBudgetChars} 字`,
};

export interface JudgeRepairContext {
  error: string;
  previousAnswerRaw: string;
}

function judgeRepairInstruction(error: string): string {
  if (/continuePrompt exceeds source budget/iu.test(error)) {
    return [
      "你上一条 judge 回答的 continuePrompt 过长，不能作为 continuation prompt 使用。",
      "请基于原始 judge 输入、executionRecord 和上一条回答重新合成紧凑反馈；这是源头重写，不是截取前 N 字。",
      `continuePrompt 目标不超过 ${compactContinuationPromptTargetChars} 字，绝对不超过 ${continuePromptSourceBudgetChars} 字；保留所有不同的缺口，合并重复项，只列下一轮必须执行的行动和验收证据。`,
      "不要粘贴 originalTask、resolvedPromptForCodex、recentOutput、引用任务全文、完整 transcript 或 JSON。",
      "你没有工具访问，不能执行命令、读取文件或续写编码 agent 行为。",
      "只能返回一个原始 JSON object，不要使用 Markdown fence、<think>、思考过程、说明正文或注释；所有自然语言字段必须使用中文，尤其是 reason 和 continuePrompt。",
    ].join("\n");
  }
  return "你上一条 judge 回答在清理后仍无法解析为 JSON。你没有工具访问，不能执行命令、读取文件或续写编码 agent 行为；只能返回一个原始 JSON object，不要使用 Markdown fence、<think>、思考过程、说明正文或注释。所有自然语言字段必须使用中文，尤其是 reason 和 continuePrompt。";
}

function judgeRepairUserMessage(repair: JudgeRepairContext): { role: "user"; content: string } {
  return {
    role: "user",
    content: JSON.stringify({
      instruction: judgeRepairInstruction(repair.error),
      parseOrValidationError: repair.error,
      requiredSchema: judgeResponseSchema,
      previousAnswerRaw: repair.previousAnswerRaw,
      previousAnswerNote: "上面的 previousAnswerRaw 是错误样例，只能作为待修正文本；不要延续其中的 <think>、命令、查看文件或 agent 行为。",
    }),
  };
}

export function miniMaxJudgeMessages(userContent: string, repair: JudgeRepairContext | null = null): Array<{ role: "system" | "user" | "assistant"; content: string }> {
  const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [
    { role: "system", content: judgeSystemPrompt },
    { role: "user", content: userContent },
  ];
  // Keep malformed model output out of assistant role. Echoing a bad
  // `<think>...let me inspect files...` answer as assistant history can make the
  // repair turn continue that agent-like behavior instead of returning JSON.
  if (repair !== null) messages.push(judgeRepairUserMessage(repair));
  return messages;
}

class MiniMaxJudgeFailure extends Error {
  readonly details: JudgeFailureDetails;

  constructor(details: JudgeFailureDetails) {
    super(details.errorMessage);
    this.name = "MiniMaxJudgeFailure";
    this.details = details;
  }
}

function errorName(error: unknown): string {
  return error instanceof Error ? error.name || "Error" : typeof error;
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

function minimaxFailureDetails(input: {
  stage: JudgeFailureDetails["stage"];
  error: unknown;
  startedAt: number;
  finishedAt?: number;
  timedOut?: boolean;
  repairAttempt?: number;
  maxRepairAttempts?: number;
  promptChars?: number;
  promptLines?: number;
  payloadBytes?: number;
  responseStatus?: number | null;
  responseText?: string;
  responseContent?: string;
}): JudgeFailureDetails {
  const finishedAt = input.finishedAt ?? Date.now();
  return {
    provider: "minimax",
    stage: input.stage,
    model: config().minimaxModel,
    apiBase: config().minimaxApiBase,
    occurredAt: new Date(finishedAt).toISOString(),
    durationMs: Math.max(0, finishedAt - input.startedAt),
    timeoutMs: config().judgeTimeoutMs,
    timedOut: input.timedOut === true,
    errorName: errorName(input.error),
    errorMessage: errorMessage(input.error),
    repairAttempt: input.repairAttempt,
    maxRepairAttempts: input.maxRepairAttempts,
    promptChars: input.promptChars,
    promptLines: input.promptLines,
    payloadBytes: input.payloadBytes,
    responseStatus: input.responseStatus,
    responseTextPreview: input.responseText === undefined ? undefined : safePreview(input.responseText, 1000),
    responseTextChars: input.responseText?.length,
    responseContentPreview: input.responseContent === undefined ? undefined : safePreview(input.responseContent, 1000),
    responseContentChars: input.responseContent?.length,
  };
}

function messagePromptStats(messages: Array<{ role: string; content: string }>): { promptChars: number; promptLines: number } {
  return messages.reduce((stats, message) => {
    const content = String(message.content || "");
    stats.promptChars += content.length;
    stats.promptLines += promptLineCount(content);
    return stats;
  }, { promptChars: 0, promptLines: 0 });
}

function miniMaxJudgeRequestBody(messages: Array<{ role: "system" | "user" | "assistant"; content: string }>): {
  payload: {
    model: string;
    temperature: number;
    max_tokens: number;
    messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
  };
  body: string;
  promptStats: { promptChars: number; promptLines: number };
  payloadBytes: number;
} {
  const payload = {
    model: config().minimaxModel,
    temperature: 0,
    max_tokens: config().judgeMaxTokens,
    messages,
  };
  const body = JSON.stringify(payload);
  return {
    payload,
    body,
    promptStats: messagePromptStats(messages),
    payloadBytes: encodedBytes(body),
  };
}

export function judgeTaskInputDiagnostics(task: QueueTask, result: CodexRunResult, includePrompt = false): Record<string, JsonValue> {
  const judgePromptContent = judgePrompt(task, result);
  const messages = miniMaxJudgeMessages(judgePromptContent, null);
  const request = miniMaxJudgeRequestBody(messages);
  return {
    provider: "minimax",
    model: config().minimaxModel,
    apiBase: config().minimaxApiBase,
    configured: config().minimaxApiKey.length > 0,
    timeoutMs: config().judgeTimeoutMs,
    repairAttempts: config().judgeRepairAttempts,
    maxTokens: config().judgeMaxTokens,
    messageCount: messages.length,
    judgePromptChars: judgePromptContent.length,
    judgePromptLines: promptLineCount(judgePromptContent),
    promptChars: request.promptStats.promptChars,
    promptLines: request.promptStats.promptLines,
    payloadBytes: request.payloadBytes,
    promptPreview: safePreview(judgePromptContent, 1200),
    ...(includePrompt ? { prompt: judgePromptContent } : {}),
  };
}

function asksToConfirmConcurrentFileInsteadOfDelivery(text: string): boolean {
  const normalized = text.replace(/\s+/gu, " ");
  const asksForConfirmation = /(请确认要我怎么处理|按安全规则[^。.!?]*先停下确认|需要先停下确认|please confirm (?:how|what|whether)|I need to stop[^.?!]*confirm)/iu.test(normalized);
  const concurrentFileContext = /(我没有修改的文件[^。.!?]*(?:modified|变成了 modified)|没有编辑这个文件|unexpected[^.?!]*(?:modified|change)|concurrent[^.?!]*(?:file|task|work)|其他任务[^。.!?]*并发|并发开发|忽略这个文件，只继续交付|只继续交付我已改的[^。.!?]*变更)/iu.test(normalized);
  return asksForConfirmation && concurrentFileContext;
}

function concurrentFileConfirmationFeedbackPrompt(task: QueueTask, reason: string): string {
  return [
    retryInstruction,
    "上一次 judge 判定为 retry：上一轮没有自主完成交付，而是在中途把并发修改文件的问题抛给用户确认。",
    `judge 未完成原因：${judgeReasonForPrompt(reason)}`,
    "这是其他任务正在并发开发，忽略这个文件，只继续交付我已改的前端相关变更。",
    "不要再要求用户选择 1/2/3，也不要检查、覆盖或回滚那个并发修改文件；只围绕自己已改的前端相关文件完成必要验证、总结交付，并在最终 response 中列出实际验证结果。",
    "如果自己的前端改动需要构建、上线或运行中验证，按项目规则执行对应 rebuild/live verification 后再声明完成；如确有阻塞，请给出具体命令和错误。",
    "原始任务摘要/按需查询：",
    compactRetryTaskContext(task),
  ].join("\n\n");
}

function rateLimitFeedbackNeedsOriginalRequirementOnly(task: QueueTask, result: CodexRunResult): boolean {
  if (!hasServiceLimitOrTransportError(judgeEvidenceText(task, result))) return false;
  const finalResponseMissing = result.finalResponse.trim().length === 0;
  const terminalNotCompleted = result.terminalStatus === "failed" || result.terminalStatus === null || result.transportClosedBeforeTerminal;
  if (!finalResponseMissing && !terminalNotCompleted) return false;
  const scopeText = [task.basePrompt, task.prompt, result.finalResponse, ...task.output.slice(-40).map((item) => item.text)].join("\n");
  return /period_sum\s*\/\s*mpu_read_num|滑动滤波前|滑动滤波后|71-Freq|71-FREQ|main\.c:282/iu.test(scopeText);
}

function originalRequirementOnlyFeedbackPrompt(task: QueueTask, reason: string): string {
  const originalTask = safePreview(task.basePrompt || userPromptForDisplay(task.prompt), 260).replace(/\s+/gu, " ").trim();
  void reason;
  return `未完成原始需求：${originalTask || "原始任务尚未完成"}。继续按照原始需求完成剩余任务。`;
}

function missingFinalResponseFeedbackPrompt(task: QueueTask, reason: string): string {
  return [
    retryInstruction,
    `上一次 attempt 未完成：${judgeReasonForPrompt(reason)}`,
    "本轮必须在最终 assistant response 中给出可见交付结果；不能只执行工具、输出 reasoning 或依赖旧 finalResponse。",
    "原始任务摘要/按需查询：",
    compactRetryTaskContext(task),
  ].join("\n\n");
}

function applyJudgeSafetyOverrides(task: QueueTask, result: CodexRunResult, judge: JudgeResult): JudgeResult {
  const currentFinalText = result.finalResponse || "";
  if (result.terminalStatus === "interrupted" && explicitUserInterrupt(task, result)) {
    return {
      ...judge,
      decision: "fail",
      confidence: Math.max(judge.confidence, 0.9),
      reason: "Codex turn 被用户请求打断。",
      raw: { previous: judge.raw ?? null, _safetyOverride: "explicit_user_interrupt" },
    };
  }
  if (rateLimitFeedbackNeedsOriginalRequirementOnly(task, result)) {
    const reason = judge.reason || result.terminalError || "任务因限流/传输中断，原始需求尚未完成。";
    return {
      ...judge,
      decision: "retry",
      confidence: Math.max(judge.confidence, 0.9),
      reason,
      continuePrompt: originalRequirementOnlyFeedbackPrompt(task, reason),
      raw: { previous: judge.raw ?? null, _safetyOverride: "original_requirement_only_feedback" },
    };
  }
  if (result.terminalStatus === "interrupted") return judge;
  if (result.terminalStatus === "failed" || result.terminalStatus === null || result.transportClosedBeforeTerminal) {
    const reason = result.terminalError
      ? `当前 attempt 未完成：${result.terminalError}`
      : "当前 attempt 未正常完成。";
    return {
      ...judge,
      decision: "retry",
      confidence: Math.max(judge.confidence, 0.9),
      reason,
      continuePrompt: missingFinalResponseFeedbackPrompt(task, reason),
      raw: { previous: judge.raw ?? null, _safetyOverride: "terminal_not_completed" },
    };
  }
  if (result.finalResponse.trim().length === 0) {
    const reason = "当前 attempt 没有生成新的最终 assistant response，不能复用旧 finalResponse 判定完成。";
    return {
      ...judge,
      decision: "retry",
      confidence: Math.max(judge.confidence, 0.92),
      reason,
      continuePrompt: missingFinalResponseFeedbackPrompt(task, reason),
      raw: { previous: judge.raw ?? null, _safetyOverride: "missing_current_final_response" },
    };
  }
  if (asksToConfirmConcurrentFileInsteadOfDelivery(currentFinalText)) {
    const reason = "最终回复停下来询问用户如何处理并发修改/无关文件，而不是自主完成自己的变更交付范围。";
    return {
      ...judge,
      decision: "retry",
      confidence: Math.max(judge.confidence, 0.94),
      reason,
      continuePrompt: concurrentFileConfirmationFeedbackPrompt(task, reason),
      raw: { previous: judge.raw ?? null, _safetyOverride: "mid_task_user_confirmation_concurrent_file" },
    };
  }
  return judge;
}

export function applyJudgeSafetyOverridesForTest(task: QueueTask, result: CodexRunResult, judge: JudgeResult): JudgeResult {
  return applyJudgeSafetyOverrides(task, result, judge);
}

export async function judgeTask(task: QueueTask, result: CodexRunResult): Promise<JudgeResult> {
  if (config().minimaxApiKey.length === 0) {
    const judge = applyJudgeSafetyOverrides(task, result, fallbackJudge(result));
    publishJudgeEvent(task, "judge-result", {
      taskId: task.id,
      attempt: task.currentAttempt,
      provider: "fallback",
      configured: false,
      judge: judge as unknown as JsonValue,
    });
    return judge;
  }
  const judgePromptContent = judgePrompt(task, result);
  const basePromptStats = { promptChars: judgePromptContent.length, promptLines: promptLineCount(judgePromptContent) };
  try {
    let lastParseError: string | null = null;
    let repairContext: JudgeRepairContext | null = null;
    for (let repairAttempt = 0; repairAttempt <= config().judgeRepairAttempts; repairAttempt += 1) {
      const messages = miniMaxJudgeMessages(judgePromptContent, repairContext);
      const request = miniMaxJudgeRequestBody(messages);
      publishJudgeEvent(task, "judge-llm-request", {
        taskId: task.id,
        attempt: task.currentAttempt,
        repairAttempt,
        requestKind: repairContext === null ? "initial" : "repair",
        provider: "minimax",
        model: config().minimaxModel,
        apiBase: config().minimaxApiBase,
        timeoutMs: config().judgeTimeoutMs,
        maxTokens: config().judgeMaxTokens,
        messageCount: messages.length,
        judgePromptChars: judgePromptContent.length,
        judgePromptLines: promptLineCount(judgePromptContent),
        promptChars: request.promptStats.promptChars,
        promptLines: request.promptStats.promptLines,
        payloadBytes: request.payloadBytes,
        // This is the exact request payload sent to MiniMax, minus headers/API key.
        requestPayload: request.payload as unknown as JsonValue,
      });
      const response = await requestMiniMaxJudge(messages, repairAttempt);
      publishJudgeEvent(task, "judge-llm-response", {
        taskId: task.id,
        attempt: task.currentAttempt,
        repairAttempt,
        provider: "minimax",
        diagnostics: response.diagnostics as unknown as JsonValue,
        responseContent: response.content,
        responseContentPreview: safePreview(response.content, 2000),
        responseRawText: response.rawText,
        responseRawTextPreview: safePreview(response.rawText, 2000),
      });
      const preDenoiseContent = response.content;
      try {
        const parsedResult = parseJudgeJson(preDenoiseContent);
        const parsed = parsedResult.value;
        if (parsedResult.source !== "direct") {
          logger("info", "judge_json_denoised", { taskId: task.id, parseSource: parsedResult.source, repairAttempt });
        }
        const decision = normalizedDecision(parsed.decision);
        const continuePrompt = parsedContinuePromptForJudge(parsed, decision);
        const confidenceRaw = Number(parsed.confidence ?? 0.5);
        const judge: JudgeResult = {
          decision,
          confidence: Number.isFinite(confidenceRaw) ? Math.max(0, Math.min(1, confidenceRaw)) : 0.5,
          reason: typeof parsed.reason === "string" ? parsed.reason : "MiniMax judge returned a decision.",
          continuePrompt,
          source: "minimax",
          raw: {
            ...(parsed as Record<string, JsonValue>),
            _parseSource: parsedResult.source,
            _repairAttempt: repairAttempt,
            _request: response.diagnostics as unknown as JsonValue,
          },
        };
        const finalJudge = applyJudgeSafetyOverrides(task, result, judge);
        publishJudgeEvent(task, "judge-result", {
          taskId: task.id,
          attempt: task.currentAttempt,
          repairAttempt,
          provider: "minimax",
          parseSource: parsedResult.source,
          minimaxJudge: judge as unknown as JsonValue,
          judge: finalJudge as unknown as JsonValue,
          safetyOverride: extractString(extractRecord(finalJudge.raw), "_safetyOverride") ?? null,
        });
        return finalJudge;
      } catch (error) {
        lastParseError = error instanceof Error ? error.message : String(error);
        publishJudgeEvent(task, "judge-json-parse-error", {
          taskId: task.id,
          attempt: task.currentAttempt,
          repairAttempt,
          provider: "minimax",
          error: safePreview(lastParseError, 1200),
          preDenoiseResponsePreview: safePreview(preDenoiseContent, 2000),
        });
        if (repairAttempt >= config().judgeRepairAttempts) {
          throw new MiniMaxJudgeFailure(minimaxFailureDetails({
            stage: /continuePrompt exceeds source budget/iu.test(lastParseError) ? "validation" : "parse",
            error: new Error(lastParseError),
            startedAt: Date.now() - response.diagnostics.durationMs,
            finishedAt: Date.now(),
            repairAttempt,
            maxRepairAttempts: config().judgeRepairAttempts,
            promptChars: response.diagnostics.promptChars,
            promptLines: response.diagnostics.promptLines,
            payloadBytes: response.diagnostics.payloadBytes,
            responseStatus: response.diagnostics.responseStatus,
            responseText: response.rawText,
            responseContent: preDenoiseContent,
          }));
        }
        logger("warn", "judge_json_parse_retry", {
          taskId: task.id,
          repairAttempt: repairAttempt + 1,
          maxRepairAttempts: config().judgeRepairAttempts,
          error: safePreview(lastParseError, 800),
          preDenoiseResponsePreview: safePreview(preDenoiseContent, 1200),
        });
        repairContext = { error: lastParseError, previousAnswerRaw: preDenoiseContent };
      }
    }
    throw new Error(lastParseError ?? "MiniMax judge exhausted JSON repair attempts");
  } catch (error) {
    const detail = error instanceof MiniMaxJudgeFailure
      ? error.details
      : minimaxFailureDetails({ stage: "unknown", error, startedAt: Date.now(), promptChars: basePromptStats.promptChars, promptLines: basePromptStats.promptLines });
    logger("warn", "judge_failed_fallback", {
      taskId: task.id,
      error: detail.errorMessage,
      errorName: detail.errorName,
      stage: detail.stage,
      timedOut: detail.timedOut,
      durationMs: detail.durationMs,
      timeoutMs: detail.timeoutMs,
      promptChars: detail.promptChars,
      promptLines: detail.promptLines,
      payloadBytes: detail.payloadBytes,
      responseStatus: detail.responseStatus,
      repairAttempt: detail.repairAttempt,
      maxRepairAttempts: detail.maxRepairAttempts,
    } as unknown as JsonValue);
    const judge = applyJudgeSafetyOverrides(task, result, fallbackJudge(result, detail));
    publishJudgeEvent(task, "judge-result", {
      taskId: task.id,
      attempt: task.currentAttempt,
      provider: "fallback",
      configured: true,
      minimaxFailure: detail as unknown as JsonValue,
      judge: judge as unknown as JsonValue,
      safetyOverride: extractString(extractRecord(judge.raw), "_safetyOverride") ?? null,
    });
    return judge;
  }
}

async function requestMiniMaxJudge(messages: Array<{ role: "system" | "user" | "assistant"; content: string }>, repairAttempt: number): Promise<MiniMaxJudgeResponse> {
  const controller = new AbortController();
  let timedOut = false;
  const startedAt = Date.now();
  const request = miniMaxJudgeRequestBody(messages);
  const { body, promptStats, payloadBytes } = request;
  const timer = setTimeout(() => {
    timedOut = true;
    controller.abort();
  }, config().judgeTimeoutMs);
  try {
    let response: Response;
    try {
      response = await fetch(`${config().minimaxApiBase}/chat/completions`, {
        method: "POST",
        headers: { authorization: `Bearer ${config().minimaxApiKey}`, "content-type": "application/json" },
        body,
        signal: controller.signal,
      });
    } catch (error) {
      throw new MiniMaxJudgeFailure(minimaxFailureDetails({
        stage: "request",
        error,
        startedAt,
        timedOut,
        repairAttempt,
        maxRepairAttempts: config().judgeRepairAttempts,
        promptChars: promptStats.promptChars,
        promptLines: promptStats.promptLines,
        payloadBytes,
      }));
    }
    let rawText: string;
    try {
      rawText = await response.text();
    } catch (error) {
      throw new MiniMaxJudgeFailure(minimaxFailureDetails({
        stage: "request",
        error,
        startedAt,
        timedOut,
        repairAttempt,
        maxRepairAttempts: config().judgeRepairAttempts,
        promptChars: promptStats.promptChars,
        promptLines: promptStats.promptLines,
        payloadBytes,
        responseStatus: response.status,
      }));
    }
    const finishedAt = Date.now();
    if (!response.ok) {
      throw new MiniMaxJudgeFailure(minimaxFailureDetails({
        stage: "http",
        error: new Error(`MiniMax HTTP ${response.status}: ${safePreview(rawText, 1000)}`),
        startedAt,
        finishedAt,
        repairAttempt,
        maxRepairAttempts: config().judgeRepairAttempts,
        promptChars: promptStats.promptChars,
        promptLines: promptStats.promptLines,
        payloadBytes,
        responseStatus: response.status,
        responseText: rawText,
      }));
    }
    let content = rawText;
    try {
      const payload = JSON.parse(rawText) as Record<string, unknown>;
      const first = Array.isArray(payload.choices) ? extractRecord(payload.choices[0]) : null;
      const message = extractRecord(first?.message);
      content = extractString(message, "content")
        ?? extractString(payload, "content")
        ?? extractString(payload, "reply")
        ?? extractString(payload, "text")
        ?? (Object.prototype.hasOwnProperty.call(payload, "decision") ? JSON.stringify(payload) : rawText);
    } catch {
      content = rawText;
    }
    return {
      rawText,
      content,
      diagnostics: {
        provider: "minimax",
        model: config().minimaxModel,
        apiBase: config().minimaxApiBase,
        durationMs: finishedAt - startedAt,
        timeoutMs: config().judgeTimeoutMs,
        promptChars: promptStats.promptChars,
        promptLines: promptStats.promptLines,
        payloadBytes,
        responseStatus: response.status,
        responseTextChars: rawText.length,
        responseContentChars: content.length,
      },
    };
  } finally {
    clearTimeout(timer);
  }
}
