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

import { lastAssistantMessage } from "./task-view";
import { resolvedReferenceContextTitle, userPromptForDisplay } from "./prompts";
import type { QueueTask, QueueTaskRequest, ReferenceInjectionRecord, ReferenceInjectionSummaryItem } from "./types";

type TaskFinder = (id: string) => QueueTask | null | Promise<QueueTask | null>;

export class ReferenceTaskLookupError extends Error {
  missingIds: string[];

  constructor(missingIds: string[]) {
    super(`referenced Code Queue task not found: ${missingIds.join(", ")}`);
    this.name = "ReferenceTaskLookupError";
    this.missingIds = [...missingIds];
  }
}

export interface ReferencesContext {
  addUniqueTaskId: (ids: string[], value: string) => void;
  findTask: TaskFinder;
  nowIso: () => string;
  referenceInjectionMaxRounds: number | null;
  referenceTaskIdsFromPrompt: (prompt: string) => string[];
}

let context: ReferencesContext | null = null;

export function configureReferences(runtimeContext: ReferencesContext): void {
  context = runtimeContext;
}

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

function taskReferenceIds(task: QueueTask): string[] {
  const ids: string[] = [];
  for (const id of task.referenceTaskIds ?? []) ctx().addUniqueTaskId(ids, id);
  for (const id of ctx().referenceTaskIdsFromPrompt(task.basePrompt || userPromptForDisplay(task.prompt))) ctx().addUniqueTaskId(ids, id);
  return ids;
}

function taskBasePrompt(task: QueueTask): string {
  return (task.basePrompt || userPromptForDisplay(task.prompt)).trimEnd();
}

function referenceSummaryItem(task: QueueTask, round: number, roundIndex: number, viaTaskId: string | null): ReferenceInjectionSummaryItem {
  const lastMessage = lastAssistantMessage(task) as Record<string, unknown>;
  const lastText = typeof lastMessage.text === "string" ? lastMessage.text : "";
  return {
    round,
    roundIndex,
    taskId: task.id,
    viaTaskId,
    status: task.status,
    providerId: task.providerId,
    executionMode: task.executionMode,
    model: task.model,
    cwd: task.cwd,
    createdAt: task.createdAt,
    updatedAt: task.updatedAt,
    promptChars: taskBasePrompt(task).length,
    finalResponseChars: lastText.trimEnd().length,
    finalResponseAt: typeof lastMessage.at === "string" ? lastMessage.at : null,
    finalResponseSource: typeof lastMessage.source === "string" ? lastMessage.source : "unknown",
    referenceTaskIds: taskReferenceIds(task),
    cliHint: `bun scripts/cli.ts codex task ${task.id}`,
  };
}

function cachedTaskFinder(finder: TaskFinder): (id: string) => Promise<QueueTask | null> {
  const cache = new Map<string, Promise<QueueTask | null>>();
  return (id: string) => {
    const existing = cache.get(id);
    if (existing !== undefined) return existing;
    const next = Promise.resolve(finder(id));
    cache.set(id, next);
    return next;
  };
}

async function collectReferenceGraph(rootIds: string[], maxRounds: number | null, finder: (id: string) => Promise<QueueTask | null>): Promise<{ items: ReferenceInjectionSummaryItem[]; tasks: QueueTask[]; truncated: boolean }> {
  const seen = new Set<string>();
  let frontier = rootIds.map((id) => ({ id, viaTaskId: null as string | null }));
  const rawItems: Array<{ task: QueueTask; depth: number; viaTaskId: string | null; discoveryIndex: number }> = [];
  let truncated = false;
  let discoveryIndex = 0;
  for (let depth = 1; frontier.length > 0; depth += 1) {
    if (maxRounds !== null && depth > maxRounds) {
      truncated = frontier.some((entry) => !seen.has(entry.id));
      break;
    }
    const next: Array<{ id: string; viaTaskId: string | null }> = [];
    for (const entry of frontier) {
      if (seen.has(entry.id)) continue;
      const task = await finder(entry.id);
      if (task === null) continue;
      seen.add(entry.id);
      discoveryIndex += 1;
      rawItems.push({ task, depth, viaTaskId: entry.viaTaskId, discoveryIndex });
      for (const childId of taskReferenceIds(task)) {
        if (!seen.has(childId)) next.push({ id: childId, viaTaskId: task.id });
      }
    }
    frontier = next;
  }
  if (frontier.some((entry) => !seen.has(entry.id))) truncated = true;
  const sorted = rawItems.sort((left, right) => {
    if (left.depth !== right.depth) return right.depth - left.depth;
    const createdDelta = Date.parse(left.task.createdAt) - Date.parse(right.task.createdAt);
    if (Number.isFinite(createdDelta) && createdDelta !== 0) return createdDelta;
    return left.discoveryIndex - right.discoveryIndex;
  });
  const depthToRound = new Map<number, number>();
  Array.from(new Set(sorted.map((item) => item.depth))).forEach((depth, index) => depthToRound.set(depth, index + 1));
  const roundCounts = new Map<number, number>();
  const items = sorted.map((item) => {
    const round = depthToRound.get(item.depth) ?? item.depth;
    const roundIndex = (roundCounts.get(round) ?? 0) + 1;
    roundCounts.set(round, roundIndex);
    return referenceSummaryItem(item.task, round, roundIndex, item.viaTaskId);
  });
  const tasks = sorted.map((item) => item.task);
  return { items, tasks, truncated };
}

function referenceRoundSeparator(round: number, totalRounds: number, items: ReferenceInjectionSummaryItem[]): string {
  const createdTimes = items.map((item) => item.createdAt).filter(Boolean).sort();
  const updatedTimes = items.map((item) => item.updatedAt).filter(Boolean).sort();
  return [
    `----- Reference Round ${round}/${totalRounds} -----`,
    `order: upstream/oldest context first; direct references appear in the last round`,
    `tasks: ${items.length}`,
    `createdRange: ${createdTimes[0] ?? "--"} -> ${createdTimes.at(-1) ?? "--"}`,
    `updatedRange: ${updatedTimes[0] ?? "--"} -> ${updatedTimes.at(-1) ?? "--"}`,
    "--------------------------------",
  ].join("\n");
}

function referencedTaskContext(task: QueueTask, summary: ReferenceInjectionSummaryItem): string {
  const lastMessage = lastAssistantMessage(task) as Record<string, unknown>;
  const lastMessageText = typeof lastMessage.text === "string" ? lastMessage.text : "";
  return [
    `## Round ${summary.round}.${summary.roundIndex} referenced task ${task.id}`,
    `- via: ${summary.viaTaskId ?? "direct"}`,
    `- status/provider/model/cwd: ${task.status} / ${task.providerId} / ${task.model} / ${task.cwd}`,
    `- created/updated: ${task.createdAt} / ${task.updatedAt}`,
    `- cli: bun scripts/cli.ts codex task ${task.id}`,
    "",
    "### Initial prompt",
    taskBasePrompt(task) || "(empty)",
    "",
    "### Final/last response",
    lastMessageText.trimEnd() || "(none yet)",
    "",
    "### Query more context",
    `Run: bun scripts/cli.ts codex task ${task.id}`,
  ].join("\n");
}

async function injectReferencedTaskContext(request: QueueTaskRequest, finder: TaskFinder = ctx().findTask, injectedAt = ctx().nowIso()): Promise<QueueTaskRequest> {
  const ids = request.referenceTaskIds ?? [];
  if (ids.length === 0 || request.prompt.includes(resolvedReferenceContextTitle)) return request;
  if (ids.length > 5) throw new Error(`referenceTaskIds supports at most 5 task ids, got ${ids.length}`);
  const cachedFinder = cachedTaskFinder(finder);
  const referencedTasks = await Promise.all(ids.map((id) => cachedFinder(id)));
  const missing = ids.filter((_id, index) => referencedTasks[index] === null);
  if (missing.length > 0) throw new ReferenceTaskLookupError(missing);
  const userPrompt = request.basePrompt ?? userPromptForDisplay(request.prompt);
  const graph = await collectReferenceGraph(ids, ctx().referenceInjectionMaxRounds, cachedFinder);
  const injection: ReferenceInjectionRecord = {
    version: 2,
    injectedAt,
    basePrompt: userPrompt,
    directReferenceTaskIds: ids,
    maxRounds: ctx().referenceInjectionMaxRounds,
    truncated: graph.truncated,
    itemCount: graph.items.length,
    items: graph.items,
  };
  const taskById = new Map(graph.tasks.map((task) => [task.id, task]));
  const groupedItems = Array.from(new Set(graph.items.map((item) => item.round))).map((round) => ({
    round,
    items: graph.items.filter((item) => item.round === round),
  }));
  const context = [
    resolvedReferenceContextTitle,
    `injectedAt: ${injectedAt}`,
    `directReferences: ${ids.join(", ")}`,
    `referenceGraphItems: ${graph.items.length}${graph.truncated ? " (truncated)" : ""}`,
    "说明：Code Queue 后端只读取每个被引用任务的结构化 basePrompt（注入前 prompt）和 final/last response；不会把历史引用注入块继续套入。多轮引用按上游/最早上下文在前、直接引用在后的顺序注入；中间执行过程不注入，只保留 CLI 查询提示。",
    "",
    ...(groupedItems.flatMap((group) => [
      referenceRoundSeparator(group.round, groupedItems.length, group.items),
      "",
      ...group.items.map((item) => {
        const task = taskById.get(item.taskId);
        return task === undefined ? "" : referencedTaskContext(task, item);
      }).filter((text) => text.length > 0),
      "",
    ])),
    "",
    "# 本次任务",
    userPrompt.trim(),
  ].join("\n");
  return { ...request, prompt: context, basePrompt: userPrompt, referenceTaskIds: ids, referenceInjection: injection };
}


export { injectReferencedTaskContext, taskReferenceIds };
