import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness";
import {
  embeddedAgentLog,
  resetAgentEventsForTest,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
import {
  onInternalDiagnosticEvent,
  resetDiagnosticEventsForTest,
  type DiagnosticEventPayload,
} from "openclaw/plugin-sdk/diagnostic-runtime";
import {
  initializeGlobalHookRunner,
  resetGlobalHookRunner,
} from "openclaw/plugin-sdk/hook-runtime";
import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  CodexAppServerEventProjector,
  type CodexAppServerEventProjectorOptions,
  type CodexAppServerToolTelemetry,
} from "./event-projector.js";
import { rememberCodexRateLimits, resetCodexRateLimitCacheForTests } from "./rate-limit-cache.js";
import { createCodexTestModel } from "./test-support.js";

const THREAD_ID = "thread-1";
const TURN_ID = "turn-1";
const tempDirs = new Set<string>();
const tinyPngBase64 =
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";

type ProjectorNotification = Parameters<CodexAppServerEventProjector["handleNotification"]>[0];

function flushDiagnosticEvents() {
  return new Promise<void>((resolve) => setImmediate(resolve));
}

function assistantMessage(text: string, timestamp: number) {
  return {
    role: "assistant" as const,
    content: [{ type: "text" as const, text }],
    api: "openai-codex-responses",
    provider: "openai-codex",
    model: "gpt-5.4-codex",
    usage: {
      input: 0,
      output: 0,
      cacheRead: 0,
      cacheWrite: 0,
      totalTokens: 0,
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
    },
    stopReason: "stop" as const,
    timestamp,
  };
}

async function createParams(): Promise<EmbeddedRunAttemptParams> {
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-projector-"));
  tempDirs.add(tempDir);
  const sessionFile = path.join(tempDir, "session.jsonl");
  SessionManager.open(sessionFile).appendMessage(assistantMessage("history", Date.now()));
  return {
    prompt: "hello",
    sessionId: "session-1",
    sessionFile,
    workspaceDir: tempDir,
    runId: "run-1",
    provider: "openai-codex",
    modelId: "gpt-5.4-codex",
    model: createCodexTestModel(),
    thinkLevel: "medium",
  } as EmbeddedRunAttemptParams;
}

async function createProjector(
  params?: EmbeddedRunAttemptParams,
  options?: CodexAppServerEventProjectorOptions,
): Promise<CodexAppServerEventProjector> {
  const resolvedParams = params ?? (await createParams());
  return new CodexAppServerEventProjector(resolvedParams, THREAD_ID, TURN_ID, options);
}

async function createProjectorWithAssistantHooks() {
  const onAssistantMessageStart = vi.fn();
  const onPartialReply = vi.fn();
  const params = await createParams();
  const projector = await createProjector({
    ...params,
    onAssistantMessageStart,
    onPartialReply,
  });
  return { onAssistantMessageStart, onPartialReply, projector };
}

beforeEach(() => {
  resetAgentEventsForTest();
  resetDiagnosticEventsForTest();
});

afterEach(async () => {
  resetAgentEventsForTest();
  resetDiagnosticEventsForTest();
  resetGlobalHookRunner();
  resetCodexRateLimitCacheForTests();
  vi.restoreAllMocks();
  vi.unstubAllEnvs();
  for (const tempDir of tempDirs) {
    await fs.rm(tempDir, { recursive: true, force: true });
  }
  tempDirs.clear();
});

async function createProjectorWithHooks() {
  const beforeCompaction = vi.fn();
  const afterCompaction = vi.fn();
  initializeGlobalHookRunner(
    createMockPluginRegistry([
      { hookName: "before_compaction", handler: beforeCompaction },
      { hookName: "after_compaction", handler: afterCompaction },
    ]),
  );
  const projector = await createProjector();
  return { projector, beforeCompaction, afterCompaction };
}

function buildEmptyToolTelemetry(): CodexAppServerToolTelemetry {
  return {
    didSendViaMessagingTool: false,
    messagingToolSentTexts: [],
    messagingToolSentMediaUrls: [],
    messagingToolSentTargets: [],
  };
}

function requireRecord(value: unknown, label: string): Record<string, unknown> {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new Error(`Expected ${label}`);
  }
  return value as Record<string, unknown>;
}

function requireArray(value: unknown, label: string): unknown[] {
  if (!Array.isArray(value)) {
    throw new Error(`Expected ${label}`);
  }
  return value;
}

function expectUsageFields(
  usage: unknown,
  expected: { input: number; output: number; cacheRead: number; total: number },
) {
  const record = requireRecord(usage, "usage");
  expect(record.input).toBe(expected.input);
  expect(record.output).toBe(expected.output);
  expect(record.cacheRead).toBe(expected.cacheRead);
  expect(record.total ?? record.totalTokens).toBe(expected.total);
}

function mockCallArg(mock: unknown, callIndex: number, argIndex: number, label: string) {
  const calls = (mock as { mock?: { calls?: unknown[][] } }).mock?.calls;
  if (!Array.isArray(calls)) {
    throw new Error(`Expected ${label} mock calls`);
  }
  const call = calls[callIndex];
  if (!call) {
    throw new Error(`Expected ${label} call ${callIndex + 1}`);
  }
  return call[argIndex];
}

function findAgentEvent(
  mock: unknown,
  params: { stream: string; phase?: string; itemId?: string; name?: string },
) {
  const calls = (mock as { mock?: { calls?: unknown[][] } }).mock?.calls;
  if (!Array.isArray(calls)) {
    throw new Error("Expected onAgentEvent mock calls");
  }
  for (const call of calls) {
    const event = requireRecord(call[0], "agent event");
    const data = requireRecord(event.data, "agent event data");
    if (
      event.stream === params.stream &&
      (!params.phase || data.phase === params.phase) &&
      (!params.itemId || data.itemId === params.itemId) &&
      (!params.name || data.name === params.name)
    ) {
      return { event, data };
    }
  }
  throw new Error(`Expected agent event ${params.stream}`);
}

function findPlanEventWithSteps(mock: unknown, steps: string[]) {
  const calls = (mock as { mock?: { calls?: unknown[][] } }).mock?.calls;
  if (!Array.isArray(calls)) {
    throw new Error("Expected onAgentEvent mock calls");
  }
  for (const call of calls) {
    const event = requireRecord(call[0], "agent event");
    if (event.stream !== "plan") {
      continue;
    }
    const data = requireRecord(event.data, "plan event data");
    if (JSON.stringify(data.steps) === JSON.stringify(steps)) {
      return data;
    }
  }
  throw new Error(`Expected plan event ${steps.join(", ")}`);
}

function forCurrentTurn(
  method: ProjectorNotification["method"],
  params: Record<string, unknown>,
): ProjectorNotification {
  return {
    method,
    params: { threadId: THREAD_ID, turnId: TURN_ID, ...params },
  } as ProjectorNotification;
}

function agentMessageDelta(delta: string, itemId = "msg-1"): ProjectorNotification {
  return forCurrentTurn("item/agentMessage/delta", { itemId, delta });
}

function appServerError(params: { message: string; willRetry: boolean }): ProjectorNotification {
  return forCurrentTurn("error", {
    error: {
      message: params.message,
      codexErrorInfo: null,
      additionalDetails: null,
    },
    willRetry: params.willRetry,
  });
}

function rateLimitsUpdated(resetsAt: number): ProjectorNotification {
  return {
    method: "account/rateLimits/updated",
    params: {
      rateLimits: {
        limitId: "codex",
        limitName: "Codex",
        primary: { usedPercent: 100, windowDurationMins: 300, resetsAt },
        secondary: null,
        credits: null,
        planType: "plus",
        rateLimitReachedType: "rate_limit_reached",
      },
    },
  } as ProjectorNotification;
}

function turnCompleted(items: unknown[] = []): ProjectorNotification {
  return turnWithStatus("completed", items);
}

function turnWithStatus(status: string, items: unknown[] = []): ProjectorNotification {
  return {
    method: "turn/completed",
    params: {
      threadId: THREAD_ID,
      turn: { id: TURN_ID, status, items },
    },
  } as ProjectorNotification;
}

describe("CodexAppServerEventProjector", () => {
  it("projects assistant deltas and usage into embedded attempt results", async () => {
    const { onAssistantMessageStart, onPartialReply, projector } =
      await createProjectorWithAssistantHooks();

    await projector.handleNotification(agentMessageDelta("hel"));
    await projector.handleNotification(agentMessageDelta("lo"));
    await projector.handleNotification(
      forCurrentTurn("thread/tokenUsage/updated", {
        tokenUsage: {
          total: {
            totalTokens: 900_000,
            inputTokens: 700_000,
            cachedInputTokens: 100_000,
            outputTokens: 100_000,
          },
          last: {
            totalTokens: 12,
            inputTokens: 5,
            cachedInputTokens: 2,
            outputTokens: 7,
          },
        },
      }),
    );
    await projector.handleNotification(
      turnCompleted([{ type: "agentMessage", id: "msg-1", text: "hello" }]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(onAssistantMessageStart).toHaveBeenCalledTimes(1);
    expect(onPartialReply).not.toHaveBeenCalled();
    expect(result.assistantTexts).toEqual(["hello"]);
    expect(result.messagesSnapshot.map((message) => message.role)).toEqual(["user", "assistant"]);
    expect(result.lastAssistant?.content).toEqual([{ type: "text", text: "hello" }]);
    expectUsageFields(result.attemptUsage, { input: 3, output: 7, cacheRead: 2, total: 12 });
    expectUsageFields(result.lastAssistant?.usage, {
      input: 3,
      output: 7,
      cacheRead: 2,
      total: 12,
    });
    expect(result.replayMetadata.replaySafe).toBe(true);
  });

  it("suppresses mirrored user prompt when the inbound message was already persisted", async () => {
    const params = await createParams();
    const projector = await createProjector({
      ...params,
      suppressNextUserMessagePersistence: true,
    });
    await projector.handleNotification(
      turnCompleted([{ type: "agentMessage", id: "msg-1", text: "retry result" }]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.messagesSnapshot.map((message) => message.role)).toEqual(["assistant"]);
    expect(JSON.stringify(result.messagesSnapshot)).not.toContain(params.prompt);
  });

  it("records canonical OpenAI Codex app-server turns with Codex local attribution", async () => {
    const params = await createParams();
    const projector = await createProjector({
      ...params,
      provider: "openai",
      modelId: "gpt-5.5",
      model: {
        ...createCodexTestModel("openai"),
        id: "gpt-5.5",
        name: "gpt-5.5",
        api: "openai-responses",
      } as EmbeddedRunAttemptParams["model"],
      runtimePlan: {
        auth: {},
        observability: {
          resolvedRef: "openai/gpt-5.5",
          provider: "openai",
          modelId: "gpt-5.5",
          harnessId: "codex",
        },
        prompt: {
          resolveSystemPromptContribution: () => undefined,
        },
        tools: {
          normalize: (tools: unknown[]) => tools,
          logDiagnostics: () => undefined,
        },
      } as unknown as EmbeddedRunAttemptParams["runtimePlan"],
    });

    await projector.handleNotification(
      turnCompleted([{ type: "agentMessage", id: "msg-1", text: "done" }]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.lastAssistant?.provider).toBe("openai-codex");
    expect(result.lastAssistant?.api).toBe("openai-codex-responses");
    expect(result.lastAssistant?.model).toBe("gpt-5.5");
  });

  it("preserves OpenAI attribution for Codex app-server OpenAI API-key fallback profiles", async () => {
    const params = await createParams();
    const projector = await createProjector({
      ...params,
      provider: "openai",
      authProfileId: "openai:work",
      modelId: "gpt-5.5",
      model: {
        ...createCodexTestModel("openai"),
        id: "gpt-5.5",
        name: "gpt-5.5",
        api: "openai-responses",
      } as EmbeddedRunAttemptParams["model"],
      runtimePlan: {
        auth: {
          providerForAuth: "openai",
          authProfileProviderForAuth: "openai",
          harnessAuthProvider: "openai-codex",
          forwardedAuthProfileId: "openai:work",
        },
        observability: {
          resolvedRef: "openai/gpt-5.5",
          provider: "openai",
          modelId: "gpt-5.5",
          harnessId: "codex",
        },
        prompt: {
          resolveSystemPromptContribution: () => undefined,
        },
        tools: {
          normalize: (tools: unknown[]) => tools,
          logDiagnostics: () => undefined,
        },
      } as unknown as EmbeddedRunAttemptParams["runtimePlan"],
    });

    await projector.handleNotification(
      turnCompleted([{ type: "agentMessage", id: "msg-1", text: "done" }]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.lastAssistant?.provider).toBe("openai");
    expect(result.lastAssistant?.api).toBe("openai-responses");
    expect(result.lastAssistant?.model).toBe("gpt-5.5");
  });

  it("preserves inbound sender metadata on the mirrored user prompt", async () => {
    const params = await createParams();
    const projector = await createProjector({
      ...params,
      messageChannel: "discord",
      messageProvider: "discord-voice",
      senderId: "user-123",
      senderName: "Test User",
      senderUsername: "testuser",
      inputProvenance: {
        kind: "external_user",
        sourceChannel: "discord",
      },
    });

    const result = projector.buildResult(buildEmptyToolTelemetry());

    const userMessage = requireRecord(result.messagesSnapshot[0], "user message");
    expect(userMessage.role).toBe("user");
    expect(userMessage.content).toBe("hello");
    expect(userMessage.sourceChannel).toBe("discord");
    expect(userMessage.senderId).toBe("user-123");
    expect(userMessage.senderName).toBe("Test User");
    expect(userMessage.senderUsername).toBe("testuser");
    expect(userMessage.senderLabel).toBe("Test User (user-123)");
    expect(userMessage.provenance).toEqual({
      kind: "external_user",
      sourceChannel: "discord",
    });
  });

  it("does not treat cumulative-only token usage as fresh context usage", async () => {
    const projector = await createProjector();

    await projector.handleNotification(agentMessageDelta("done"));
    await projector.handleNotification(
      forCurrentTurn("thread/tokenUsage/updated", {
        tokenUsage: {
          total: {
            totalTokens: 1_000_000,
            inputTokens: 999_000,
            cachedInputTokens: 500,
            outputTokens: 500,
          },
        },
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.assistantTexts).toEqual(["done"]);
    expect(result.attemptUsage).toBeUndefined();
    expectUsageFields(result.lastAssistant?.usage, {
      input: 0,
      output: 0,
      cacheRead: 0,
      total: 0,
    });
  });

  it("uses raw assistant response items when turn completion omits items", async () => {
    const projector = await createProjector();

    await projector.handleNotification(
      forCurrentTurn("rawResponseItem/completed", {
        item: {
          type: "message",
          id: "raw-1",
          role: "assistant",
          content: [{ type: "output_text", text: "OK from raw" }],
        },
      }),
    );
    await projector.handleNotification(turnCompleted());

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.assistantTexts).toEqual(["OK from raw"]);
    expect(result.lastAssistant?.content).toEqual([{ type: "text", text: "OK from raw" }]);
  });

  it("attaches native Codex image-generation saved paths as reply media", async () => {
    const projector = await createProjector();
    const savedPath = "/tmp/codex-home/generated_images/session-1/ig_123.png";

    await projector.handleNotification(
      turnCompleted([
        {
          type: "imageGeneration",
          id: "ig_123",
          status: "completed",
          revisedPrompt: "A tiny blue square",
          result: "Zm9v",
          savedPath,
        },
      ]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.assistantTexts).toStrictEqual([]);
    expect(result.toolMediaUrls).toEqual([savedPath]);
    expect(result.replayMetadata).toStrictEqual({
      hadPotentialSideEffects: true,
      replaySafe: false,
    });
  });

  it("saves raw Codex image-generation results as reply media", async () => {
    const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-media-state-"));
    tempDirs.add(stateDir);
    vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
    const projector = await createProjector();

    await projector.handleNotification(
      forCurrentTurn("rawResponseItem/completed", {
        item: {
          type: "image_generation_call",
          id: "ig_raw_1",
          status: "generating",
          result: tinyPngBase64,
          revised_prompt: "A tiny blue square",
        },
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());
    const mediaUrl = result.toolMediaUrls?.[0];

    expect(result.assistantTexts).toStrictEqual([]);
    expect(result.toolMediaUrls).toHaveLength(1);
    expect(mediaUrl).toContain(`${path.sep}media${path.sep}tool-image-generation${path.sep}`);
    expect(mediaUrl?.endsWith(".png")).toBe(true);
    await expect(fs.readFile(mediaUrl ?? "")).resolves.toEqual(
      Buffer.from(tinyPngBase64, "base64"),
    );
    expect(result.replayMetadata).toStrictEqual({
      hadPotentialSideEffects: true,
      replaySafe: false,
    });
  });

  it("keeps raw image-generation results replay-invalid when media save fails", async () => {
    const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
    const projector = await createProjector({
      ...(await createParams()),
      config: { agents: { defaults: { mediaMaxMb: 0.000001 } } },
    } as EmbeddedRunAttemptParams);

    await projector.handleNotification(
      forCurrentTurn("rawResponseItem/completed", {
        item: {
          type: "image_generation_call",
          id: "ig_raw_capped",
          status: "completed",
          result: tinyPngBase64,
        },
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.toolMediaUrls).toBeUndefined();
    expect(result.replayMetadata).toStrictEqual({
      hadPotentialSideEffects: true,
      replaySafe: false,
    });
    expect(warn).toHaveBeenCalledWith(
      "codex app-server raw image generation result exceeds media limit",
      expect.objectContaining({ itemId: "ig_raw_capped" }),
    );
  });

  it("dedupes raw and typed Codex image-generation media for the same item", async () => {
    const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-media-state-"));
    tempDirs.add(stateDir);
    vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
    const projector = await createProjector();
    const savedPath = "/tmp/codex-home/generated_images/session-1/ig_123.png";

    await projector.handleNotification(
      forCurrentTurn("rawResponseItem/completed", {
        item: {
          type: "image_generation_call",
          id: "ig_123",
          status: "generating",
          result: tinyPngBase64,
        },
      }),
    );
    await projector.handleNotification(
      turnCompleted([
        {
          type: "imageGeneration",
          id: "ig_123",
          status: "completed",
          revisedPrompt: "A tiny blue square",
          result: tinyPngBase64,
          savedPath,
        },
      ]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.toolMediaUrls).toHaveLength(1);
    expect(result.toolMediaUrls?.[0]).not.toBe(savedPath);
  });

  it("preserves distinct raw image-generation items with identical image bytes", async () => {
    const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-media-state-"));
    tempDirs.add(stateDir);
    vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
    const projector = await createProjector();

    for (const id of ["ig_raw_1", "ig_raw_2"]) {
      await projector.handleNotification(
        forCurrentTurn("rawResponseItem/completed", {
          item: {
            type: "image_generation_call",
            id,
            status: "generating",
            result: tinyPngBase64,
          },
        }),
      );
    }

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.toolMediaUrls).toHaveLength(2);
    expect(new Set(result.toolMediaUrls)).toHaveLength(2);
  });

  it("does not append native Codex image-generation media after explicit media delivery", async () => {
    const projector = await createProjector();
    const savedPath = "/tmp/codex-home/generated_images/session-1/ig_123.png";

    await projector.handleNotification(
      turnCompleted([
        {
          type: "imageGeneration",
          id: "ig_123",
          status: "completed",
          revisedPrompt: null,
          result: "Zm9v",
          savedPath,
        },
      ]),
    );

    const result = projector.buildResult({
      ...buildEmptyToolTelemetry(),
      messagingToolSentMediaUrls: [savedPath],
      toolMediaUrls: [],
    });

    expect(result.toolMediaUrls).toStrictEqual([]);
  });

  it("does not promote repeated tool progress text to the final assistant reply", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      onToolResult,
    });

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: {
          type: "commandExecution",
          id: "cmd-1",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "inProgress",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: null,
          durationMs: null,
        },
      }),
    );
    const toolProgressText = (mockCallArg(onToolResult, 0, 0, "onToolResult") as { text?: string })
      .text;
    expect(toolProgressText).toBe("🛠️ `run tests (workspace)`");

    await projector.handleNotification(
      forCurrentTurn("rawResponseItem/completed", {
        item: {
          type: "message",
          id: "raw-tool-progress",
          role: "assistant",
          content: [{ type: "output_text", text: toolProgressText }],
        },
      }),
    );
    await projector.handleNotification(turnCompleted());

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.assistantTexts).toEqual([]);
    expect(result.lastAssistant).toBeUndefined();
  });

  it("does not treat app-server interrupted status as a user cancellation by itself", async () => {
    const projector = await createProjector();

    await projector.handleNotification(turnWithStatus("interrupted"));

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.aborted).toBe(false);
    expect(result.externalAbort).toBe(false);
    expect(result.timedOut).toBe(false);
    expect(result.promptError).toBeNull();
    expect(result.assistantTexts).toEqual([]);
    expect(result.lastAssistant).toBeUndefined();
  });

  it("keeps sparse successful bash output eligible for the no-visible-answer guard", async () => {
    const projector = await createProjector();

    await projector.handleNotification(
      turnWithStatus("interrupted", [
        {
          type: "commandExecution",
          id: "cmd-empty-output",
          command:
            "ps -eo pid,ppid,stat,cmd | rg 'venv-roadmap|pytest|run_security_contract_validation|validate_public_install|git push|apply_patch' || true",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "",
          exitCode: 0,
          durationMs: 42,
        },
      ]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.aborted).toBe(false);
    expect(result.assistantTexts).toEqual([]);
    expect(result.toolMetas).toEqual([
      expect.objectContaining({ toolName: "bash", meta: expect.stringContaining("workspace") }),
    ]);
  });

  it("keeps explicit cancellation marked aborted for interrupted tool-only turns", async () => {
    const projector = await createProjector();
    projector.markAborted();

    await projector.handleNotification(
      turnWithStatus("interrupted", [
        {
          type: "commandExecution",
          id: "cmd-cancelled",
          command: "/bin/bash -lc true",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "",
          exitCode: 0,
          durationMs: 12,
        },
      ]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());
    expect(result.aborted).toBe(true);
    expect(result.assistantTexts).toEqual([]);
  });

  it("does not fail a completed reply after a retryable app-server error notification", async () => {
    const projector = await createProjector();

    await projector.handleNotification(agentMessageDelta("still working"));
    await projector.handleNotification(
      appServerError({ message: "stream disconnected", willRetry: true }),
    );
    await projector.handleNotification(
      turnCompleted([{ type: "agentMessage", id: "msg-1", text: "final answer" }]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.assistantTexts).toEqual(["final answer"]);
    expect(result.promptError).toBeNull();
    expect(result.promptErrorSource).toBeNull();
    expect(result.lastAssistant?.stopReason).toBe("stop");
    expect(result.lastAssistant?.errorMessage).toBeUndefined();
  });

  it("uses nested app-server error messages for terminal errors", async () => {
    const projector = await createProjector();

    await projector.handleNotification(
      appServerError({ message: "stream failed permanently", willRetry: false }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.promptError).toBe("stream failed permanently");
    expect(result.promptErrorSource).toBe("prompt");
    expect(result.lastAssistant).toBeUndefined();
  });

  it("uses Codex rate-limit resets for usage-limit app-server errors", async () => {
    const projector = await createProjector();
    const resetsAt = Math.ceil(Date.now() / 1000) + 120;

    await projector.handleNotification(rateLimitsUpdated(resetsAt));
    await projector.handleNotification(
      forCurrentTurn("error", {
        error: {
          message: "You've reached your usage limit.",
          codexErrorInfo: "usageLimitExceeded",
          additionalDetails: null,
        },
        willRetry: false,
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.promptError).toContain("You've reached your Codex subscription usage limit.");
    expect(result.promptError).toContain("Next reset in");
    expect(result.promptError).toContain("Wait until the reset time");
    expect(result.promptErrorSource).toBe("prompt");
  });

  it("uses Codex rate-limit resets for failed turns", async () => {
    const projector = await createProjector();
    const resetsAt = Math.ceil(Date.now() / 1000) + 120;

    await projector.handleNotification(rateLimitsUpdated(resetsAt));
    await projector.handleNotification(
      forCurrentTurn("turn/completed", {
        turn: {
          id: TURN_ID,
          status: "failed",
          error: {
            message: "You've reached your usage limit.",
            codexErrorInfo: "usageLimitExceeded",
            additionalDetails: null,
          },
          items: [],
        },
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.promptError).toContain("You've reached your Codex subscription usage limit.");
    expect(result.promptError).toContain("Next reset in");
    expect(result.promptErrorSource).toBe("prompt");
  });

  it("uses a recent Codex rate-limit snapshot when failed turns omit reset details", async () => {
    const projector = await createProjector();
    const resetsAt = Math.ceil(Date.now() / 1000) + 120;
    rememberCodexRateLimits({
      rateLimits: {
        limitId: "codex",
        limitName: "Codex",
        primary: { usedPercent: 100, windowDurationMins: 300, resetsAt },
        secondary: null,
        credits: null,
        planType: "plus",
        rateLimitReachedType: "rate_limit_reached",
      },
      rateLimitsByLimitId: null,
    });

    await projector.handleNotification(
      forCurrentTurn("turn/completed", {
        turn: {
          id: TURN_ID,
          status: "failed",
          error: {
            message: "You've reached your usage limit.",
            codexErrorInfo: "usageLimitExceeded",
            additionalDetails: null,
          },
          items: [],
        },
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.promptError).toContain("You've reached your Codex subscription usage limit.");
    expect(result.promptError).toContain("Next reset in");
    expect(result.promptErrorSource).toBe("prompt");
  });

  it("preserves Codex retry hints when failed turns omit structured reset details", async () => {
    const projector = await createProjector();

    await projector.handleNotification(
      forCurrentTurn("turn/completed", {
        turn: {
          id: TURN_ID,
          status: "failed",
          error: {
            message:
              "You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at May 11th, 2026 9:00 AM.",
            codexErrorInfo: "usageLimitExceeded",
            additionalDetails: null,
          },
          items: [],
        },
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.promptError).toContain("You've reached your Codex subscription usage limit.");
    expect(result.promptError).toContain("Codex says to try again at May 11th, 2026 9:00 AM.");
    expect(result.promptError).not.toContain("Codex did not return a reset time");
    expect(result.promptErrorSource).toBe("prompt");
  });

  it("normalizes snake_case current token usage fields", async () => {
    const projector = await createProjector();

    await projector.handleNotification(agentMessageDelta("done"));
    await projector.handleNotification(
      forCurrentTurn("thread/tokenUsage/updated", {
        tokenUsage: {
          total: { total_tokens: 1_000_000 },
          last_token_usage: {
            total_tokens: 17,
            input_tokens: 8,
            cached_input_tokens: 3,
            output_tokens: 9,
          },
        },
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expectUsageFields(result.attemptUsage, { input: 5, output: 9, cacheRead: 3, total: 17 });
    expectUsageFields(result.lastAssistant?.usage, {
      input: 5,
      output: 9,
      cacheRead: 3,
      total: 17,
    });
  });

  it("keeps intermediate agentMessage items out of the final visible reply", async () => {
    const { onAssistantMessageStart, onPartialReply, projector } =
      await createProjectorWithAssistantHooks();

    await projector.handleNotification(
      agentMessageDelta(
        "checking thread context; then post a tight progress reply here.",
        "msg-commentary",
      ),
    );
    await projector.handleNotification(
      agentMessageDelta(
        "release fixes first. please drop affected PRs, failing checks, and blockers here.",
        "msg-final",
      ),
    );
    await projector.handleNotification(
      turnCompleted([
        {
          type: "agentMessage",
          id: "msg-commentary",
          text: "checking thread context; then post a tight progress reply here.",
        },
        {
          type: "agentMessage",
          id: "msg-final",
          text: "release fixes first. please drop affected PRs, failing checks, and blockers here.",
        },
      ]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(onAssistantMessageStart).toHaveBeenCalledTimes(1);
    expect(onPartialReply).not.toHaveBeenCalled();
    expect(result.assistantTexts).toEqual([
      "release fixes first. please drop affected PRs, failing checks, and blockers here.",
    ]);
    expect(result.lastAssistant?.content).toEqual([
      {
        type: "text",
        text: "release fixes first. please drop affected PRs, failing checks, and blockers here.",
      },
    ]);
    expect(JSON.stringify(result.messagesSnapshot)).not.toContain("checking thread context");
  });

  it("streams commentary agent messages as keyed progress events", async () => {
    const onAgentEvent = vi.fn();
    const onPartialReply = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      onAgentEvent,
      onPartialReply,
    });

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: {
          type: "agentMessage",
          id: "msg-commentary",
          phase: "commentary",
          text: "",
        },
      }),
    );
    await projector.handleNotification(agentMessageDelta("Checking", "msg-commentary"));
    await projector.handleNotification(
      agentMessageDelta(" the app-server stream", "msg-commentary"),
    );
    await projector.handleNotification(
      turnCompleted([
        {
          type: "agentMessage",
          id: "msg-commentary",
          phase: "commentary",
          text: "Checking the app-server stream",
        },
        {
          type: "agentMessage",
          id: "msg-final",
          phase: "final_answer",
          text: "final answer",
        },
      ]),
    );

    const progressEvents = onAgentEvent.mock.calls
      .map((call) => call[0])
      .filter((event) => event.stream === "item" && event.data.kind === "preamble");

    expect(onPartialReply).not.toHaveBeenCalled();
    expect(progressEvents.map((event) => event.data)).toEqual([
      {
        itemId: "msg-commentary",
        kind: "preamble",
        title: "Preamble",
        phase: "update",
        progressText: "Checking",
        source: "codex-app-server",
      },
      {
        itemId: "msg-commentary",
        kind: "preamble",
        title: "Preamble",
        phase: "update",
        progressText: "Checking the app-server stream",
        source: "codex-app-server",
      },
    ]);

    const result = projector.buildResult(buildEmptyToolTelemetry());
    expect(result.assistantTexts).toEqual(["final answer"]);
  });

  it("does not resolve commentary-phase assistant text as the final reply", async () => {
    const projector = await createProjector();

    await projector.handleNotification(
      turnCompleted([
        {
          type: "agentMessage",
          id: "msg-final",
          phase: "final_answer",
          text: "final answer",
        },
        {
          type: "agentMessage",
          id: "msg-commentary",
          phase: "commentary",
          text: "I am checking one more thing.",
        },
      ]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.assistantTexts).toEqual(["final answer"]);
  });

  it("ignores notifications for other turns", async () => {
    const projector = await createProjector();

    await projector.handleNotification({
      method: "item/agentMessage/delta",
      params: { threadId: THREAD_ID, turnId: "turn-2", itemId: "msg-1", delta: "wrong" },
    });

    const result = projector.buildResult(buildEmptyToolTelemetry());
    expect(result.assistantTexts).toStrictEqual([]);
  });

  it("ignores notifications that omit top-level thread and turn ids", async () => {
    const projector = await createProjector();

    await projector.handleNotification({
      method: "turn/completed",
      params: {
        turn: {
          id: TURN_ID,
          status: "completed",
          items: [{ type: "agentMessage", id: "msg-1", text: "wrong turn" }],
        },
      },
    });

    const result = projector.buildResult(buildEmptyToolTelemetry());
    expect(result.assistantTexts).toStrictEqual([]);
    expect(result.lastAssistant).toBeUndefined();
  });

  it("preserves sessions_yield detection in attempt results", () => {
    const projector = new CodexAppServerEventProjector(
      {
        prompt: "hello",
        sessionId: "session-1",
        sessionFile: "/tmp/session.jsonl",
        workspaceDir: "/tmp",
        runId: "run-1",
        provider: "openai-codex",
        modelId: "gpt-5.4-codex",
        model: createCodexTestModel(),
        thinkLevel: "medium",
      } as EmbeddedRunAttemptParams,
      THREAD_ID,
      TURN_ID,
    );

    const result = projector.buildResult(buildEmptyToolTelemetry(), { yieldDetected: true });

    expect(result.yieldDetected).toBe(true);
  });

  it("projects guardian review lifecycle details into agent events", async () => {
    const onAgentEvent = vi.fn();
    const projector = await createProjector({ ...(await createParams()), onAgentEvent });

    await projector.handleNotification(
      forCurrentTurn("item/autoApprovalReview/started", {
        reviewId: "review-1",
        targetItemId: "cmd-1",
        review: { status: "inProgress" },
        action: {
          type: "execve",
          source: "shell",
          program: "/bin/printf",
          argv: ["printf", "hello"],
          cwd: "/tmp",
        },
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/autoApprovalReview/completed", {
        reviewId: "review-1",
        targetItemId: "cmd-1",
        decisionSource: "agent",
        review: {
          status: "approved",
          riskLevel: "low",
          userAuthorization: "high",
          rationale: "Benign local probe.",
        },
        action: {
          type: "execve",
          source: "shell",
          program: "/bin/printf",
          argv: ["printf", "hello"],
          cwd: "/tmp",
        },
      }),
    );

    const started = findAgentEvent(onAgentEvent, {
      stream: "codex_app_server.guardian",
      phase: "started",
    }).data;
    expect(started.reviewId).toBe("review-1");
    expect(started.targetItemId).toBe("cmd-1");
    expect(started.status).toBe("inProgress");
    expect(started.actionType).toBe("execve");
    const completed = findAgentEvent(onAgentEvent, {
      stream: "codex_app_server.guardian",
      phase: "completed",
    }).data;
    expect(completed.reviewId).toBe("review-1");
    expect(completed.targetItemId).toBe("cmd-1");
    expect(completed.decisionSource).toBe("agent");
    expect(completed.status).toBe("approved");
    expect(completed.riskLevel).toBe("low");
    expect(completed.userAuthorization).toBe("high");
    expect(completed.rationale).toBe("Benign local probe.");
    expect(completed.actionType).toBe("execve");
    expect(
      projector.buildResult(buildEmptyToolTelemetry()).didSendDeterministicApprovalPrompt,
    ).toBe(false);
  });

  it("projects reasoning end, plan updates, compaction state, and tool metadata", async () => {
    const onReasoningStream = vi.fn();
    const onReasoningEnd = vi.fn();
    const onAgentEvent = vi.fn();
    const params = {
      ...(await createParams()),
      onReasoningStream,
      onReasoningEnd,
      onAgentEvent,
    };
    const projector = await createProjector(params);

    await projector.handleNotification(
      forCurrentTurn("item/reasoning/textDelta", { itemId: "reason-1", delta: "thinking" }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/plan/delta", { itemId: "plan-1", delta: "- inspect\n" }),
    );
    await projector.handleNotification(
      forCurrentTurn("turn/plan/updated", {
        explanation: "next",
        plan: [{ step: "patch", status: "in_progress" }],
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: { type: "contextCompaction", id: "compact-1" },
      }),
    );
    expect(projector.isCompacting()).toBe(true);
    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: { type: "contextCompaction", id: "compact-1" },
      }),
    );
    expect(projector.isCompacting()).toBe(false);
    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "dynamicToolCall",
          id: "tool-1",
          tool: "sessions_send",
          status: "completed",
        },
      }),
    );
    await projector.handleNotification(turnCompleted());

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(onReasoningStream).toHaveBeenCalledWith({
      text: "thinking",
      isReasoningSnapshot: true,
    });
    expect(onReasoningEnd).toHaveBeenCalledTimes(1);
    expect(findPlanEventWithSteps(onAgentEvent, ["patch (in_progress)"]).steps).toEqual([
      "patch (in_progress)",
    ]);
    expect(findAgentEvent(onAgentEvent, { stream: "compaction", phase: "start" }).data.itemId).toBe(
      "compact-1",
    );
    expect(findAgentEvent(onAgentEvent, { stream: "compaction", phase: "end" }).data).toMatchObject(
      {
        itemId: "compact-1",
        completed: true,
      },
    );
    expect(result.toolMetas).toEqual([{ toolName: "sessions_send" }]);
    expect(result.messagesSnapshot.map((message) => message.role)).toEqual([
      "user",
      "assistant",
      "assistant",
    ]);
    expect(JSON.stringify(result.messagesSnapshot[1])).toContain("Codex reasoning");
    expect(JSON.stringify(result.messagesSnapshot[2])).toContain("Codex plan");
    expect(requireRecord(result.itemLifecycle, "item lifecycle").compactionCount).toBe(1);
  });

  it("streams accumulated reasoning snapshots grouped by Codex reasoning indexes", async () => {
    const onReasoningStream = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      onReasoningStream,
    });

    await projector.handleNotification(
      forCurrentTurn("item/reasoning/textDelta", {
        itemId: "reason-1",
        contentIndex: 1,
        delta: "Checking ",
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/reasoning/textDelta", {
        itemId: "reason-1",
        contentIndex: 0,
        delta: "Reading ",
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/reasoning/textDelta", {
        itemId: "reason-1",
        contentIndex: 0,
        delta: "files",
      }),
    );

    expect(onReasoningStream).toHaveBeenCalledTimes(3);
    expect(onReasoningStream).toHaveBeenNthCalledWith(1, {
      text: "Checking ",
      isReasoningSnapshot: true,
    });
    expect(onReasoningStream).toHaveBeenNthCalledWith(2, {
      text: "Reading \n\nChecking ",
      isReasoningSnapshot: true,
    });
    expect(onReasoningStream).toHaveBeenNthCalledWith(3, {
      text: "Reading files\n\nChecking ",
      isReasoningSnapshot: true,
    });
  });

  it("streams accumulated reasoning summaries grouped by summary section", async () => {
    const onReasoningStream = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      onReasoningStream,
    });

    await projector.handleNotification(
      forCurrentTurn("item/reasoning/summaryTextDelta", {
        itemId: "reason-1",
        summaryIndex: 1,
        delta: "Second",
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/reasoning/summaryTextDelta", {
        itemId: "reason-1",
        summaryIndex: 0,
        delta: "First ",
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/reasoning/summaryTextDelta", {
        itemId: "reason-1",
        summaryIndex: 0,
        delta: "section",
      }),
    );

    expect(onReasoningStream).toHaveBeenCalledTimes(3);
    expect(onReasoningStream).toHaveBeenNthCalledWith(1, {
      text: "Second",
      isReasoningSnapshot: true,
    });
    expect(onReasoningStream).toHaveBeenNthCalledWith(2, {
      text: "First \n\nSecond",
      isReasoningSnapshot: true,
    });
    expect(onReasoningStream).toHaveBeenNthCalledWith(3, {
      text: "First section\n\nSecond",
      isReasoningSnapshot: true,
    });
  });

  it("synthesizes normalized tool progress for Codex-native tool items", async () => {
    const onAgentEvent = vi.fn();
    const projector = await createProjector({ ...(await createParams()), onAgentEvent });
    const diagnosticEvents: DiagnosticEventPayload[] = [];
    const unsubscribe = onInternalDiagnosticEvent((event) => diagnosticEvents.push(event));

    try {
      await projector.handleNotification(
        forCurrentTurn("item/started", {
          item: {
            type: "commandExecution",
            id: "cmd-1",
            command: "pnpm test extensions/codex",
            cwd: "/workspace",
            processId: null,
            source: "agent",
            status: "inProgress",
            commandActions: [],
            aggregatedOutput: null,
            exitCode: null,
            durationMs: null,
          },
        }),
      );
      await projector.handleNotification(
        forCurrentTurn("item/completed", {
          item: {
            type: "commandExecution",
            id: "cmd-1",
            command: "pnpm test extensions/codex",
            cwd: "/workspace",
            processId: null,
            source: "agent",
            status: "completed",
            commandActions: [],
            aggregatedOutput: "ok",
            exitCode: 0,
            durationMs: 42,
          },
        }),
      );
      await flushDiagnosticEvents();
    } finally {
      unsubscribe();
    }

    const itemStart = findAgentEvent(onAgentEvent, {
      stream: "item",
      phase: "start",
      itemId: "cmd-1",
    }).data;
    expect(itemStart.kind).toBe("command");
    expect(itemStart.name).toBe("bash");
    expect(itemStart.suppressChannelProgress).toBe(true);
    const toolStart = findAgentEvent(onAgentEvent, {
      stream: "tool",
      phase: "start",
      itemId: "cmd-1",
      name: "bash",
    }).data;
    expect(toolStart.toolCallId).toBe("cmd-1");
    expect(toolStart.args).toEqual({ command: "pnpm test extensions/codex", cwd: "/workspace" });
    const toolResult = findAgentEvent(onAgentEvent, {
      stream: "tool",
      phase: "result",
      itemId: "cmd-1",
      name: "bash",
    }).data;
    expect(toolResult.toolCallId).toBe("cmd-1");
    expect(toolResult.status).toBe("completed");
    expect(toolResult.isError).toBe(false);
    const toolResultPayload = requireRecord(toolResult.result, "tool result payload");
    expect(toolResultPayload.exitCode).toBe(0);
    expect(toolResultPayload.durationMs).toBe(42);
    const toolDiagnosticEvents = diagnosticEvents.filter(
      (
        event,
      ): event is Extract<
        DiagnosticEventPayload,
        {
          type:
            | "tool.execution.started"
            | "tool.execution.completed"
            | "tool.execution.error"
            | "tool.execution.blocked";
        }
      > => event.type.startsWith("tool.execution."),
    );
    expect(
      toolDiagnosticEvents.map((event) => ({
        type: event.type,
        toolName: event.toolName,
        toolCallId: event.toolCallId,
        durationMs: "durationMs" in event ? event.durationMs : undefined,
      })),
    ).toEqual([
      {
        type: "tool.execution.started",
        toolName: "bash",
        toolCallId: "cmd-1",
        durationMs: undefined,
      },
      {
        type: "tool.execution.completed",
        toolName: "bash",
        toolCallId: "cmd-1",
        durationMs: 42,
      },
    ]);
    const result = projector.buildResult(buildEmptyToolTelemetry());
    expect(result.messagesSnapshot.map((message) => message.role)).toEqual([
      "user",
      "assistant",
      "toolResult",
    ]);
    const assistant = requireRecord(result.messagesSnapshot[1], "assistant tool call message");
    expect(assistant.role).toBe("assistant");
    const assistantContent = requireArray(assistant.content, "assistant content");
    expect(assistantContent[0]).toEqual({
      type: "toolCall",
      id: "cmd-1",
      name: "bash",
      arguments: { command: "pnpm test extensions/codex", cwd: "/workspace" },
      input: { command: "pnpm test extensions/codex", cwd: "/workspace" },
    });
    const toolResultMessage = requireRecord(result.messagesSnapshot[2], "tool result message");
    expect(toolResultMessage.role).toBe("toolResult");
    expect(toolResultMessage.toolCallId).toBe("cmd-1");
    expect(toolResultMessage.toolName).toBe("bash");
    expect(toolResultMessage.isError).toBe(false);
    const toolResultContent = requireArray(toolResultMessage.content, "tool result content");
    const toolResultContentItem = requireRecord(toolResultContent[0], "tool result content item");
    expect(toolResultContentItem.type).toBe("toolResult");
    expect(toolResultContentItem.id).toBe("cmd-1");
    expect(toolResultContentItem.name).toBe("bash");
    expect(toolResultContentItem.toolName).toBe("bash");
    expect(toolResultContentItem.toolCallId).toBe("cmd-1");
    expect(toolResultContentItem.content).toBe("ok");
  });

  it("synthesizes native tool progress from turn completion snapshots", async () => {
    const onAgentEvent = vi.fn();
    const onToolResult = vi.fn();
    const trajectoryRecorder = {
      filePath: "trajectory.jsonl",
      recordEvent: vi.fn(),
      flush: vi.fn(async () => undefined),
    };
    const projector = await createProjector(
      {
        ...(await createParams()),
        verboseLevel: "on",
        onAgentEvent,
        onToolResult,
      },
      {
        trajectoryRecorder,
      },
    );

    await projector.handleNotification(
      turnCompleted([
        {
          type: "commandExecution",
          id: "cmd-snapshot",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "ok",
          exitCode: 0,
          durationMs: 42,
        },
      ]),
    );

    const itemStart = findAgentEvent(onAgentEvent, {
      stream: "item",
      phase: "start",
      itemId: "cmd-snapshot",
    }).data;
    expect(itemStart.kind).toBe("command");
    expect(itemStart.name).toBe("bash");
    expect(itemStart.suppressChannelProgress).toBe(true);
    const toolStart = findAgentEvent(onAgentEvent, {
      stream: "tool",
      phase: "start",
      itemId: "cmd-snapshot",
      name: "bash",
    }).data;
    expect(toolStart.args).toEqual({ command: "pnpm test extensions/codex", cwd: "/workspace" });
    const toolResult = findAgentEvent(onAgentEvent, {
      stream: "tool",
      phase: "result",
      itemId: "cmd-snapshot",
      name: "bash",
    }).data;
    expect(toolResult.status).toBe("completed");
    expect(toolResult.isError).toBe(false);
    expect(onToolResult).toHaveBeenCalledWith({
      text: "🛠️ `run tests (workspace)`",
    });
    expect(trajectoryRecorder.recordEvent).toHaveBeenCalledWith("tool.call", {
      threadId: THREAD_ID,
      turnId: TURN_ID,
      itemId: "cmd-snapshot",
      toolCallId: "cmd-snapshot",
      name: "bash",
      arguments: { command: "pnpm test extensions/codex", cwd: "/workspace" },
    });
    expect(trajectoryRecorder.recordEvent).toHaveBeenCalledWith("tool.result", {
      threadId: THREAD_ID,
      turnId: TURN_ID,
      itemId: "cmd-snapshot",
      toolCallId: "cmd-snapshot",
      name: "bash",
      status: "completed",
      isError: false,
      result: { status: "completed", exitCode: 0, durationMs: 42 },
      output: "ok",
    });
  });

  it("uses streamed command output when final command snapshots omit aggregated output", async () => {
    const onAgentEvent = vi.fn();
    const trajectoryRecorder = {
      filePath: "trajectory.jsonl",
      recordEvent: vi.fn(),
      flush: vi.fn(async () => undefined),
    };
    const projector = await createProjector(
      {
        ...(await createParams()),
        onAgentEvent,
      },
      {
        trajectoryRecorder,
      },
    );

    await projector.handleNotification(
      forCurrentTurn("item/commandExecution/outputDelta", {
        itemId: "cmd-1",
        delta: "status passed\n",
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/commandExecution/outputDelta", {
        itemId: "cmd-1",
        delta: "json /tmp/scenario.json\n",
      }),
    );
    await projector.handleNotification(
      turnCompleted([
        {
          type: "commandExecution",
          id: "cmd-1",
          command: "python scripts/run_demo_scenario.py",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: 0,
          durationMs: 42,
        },
      ]),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());
    const toolResultMessage = requireRecord(result.messagesSnapshot[2], "tool result message");
    const toolResultContent = requireArray(toolResultMessage.content, "tool result content");
    const toolResultContentItem = requireRecord(toolResultContent[0], "tool result content item");
    expect(toolResultContentItem.content).toBe("status passed\njson /tmp/scenario.json");
    expect(trajectoryRecorder.recordEvent).toHaveBeenCalledWith(
      "tool.result",
      expect.objectContaining({
        itemId: "cmd-1",
        output: "status passed\njson /tmp/scenario.json",
      }),
    );
    const toolResult = findAgentEvent(onAgentEvent, {
      stream: "tool",
      phase: "result",
      itemId: "cmd-1",
      name: "bash",
    }).data;
    expect(toolResult.result).toEqual({ status: "completed", exitCode: 0, durationMs: 42 });
  });

  it("uses streamed command output for failed native tool errors", async () => {
    const projector = await createProjector();

    await projector.handleNotification(
      forCurrentTurn("item/commandExecution/outputDelta", {
        itemId: "cmd-streamed-failure",
        delta: "fatal: missing fixture\n",
      }),
    );
    await projector.handleNotification(
      turnCompleted([
        {
          type: "commandExecution",
          id: "cmd-streamed-failure",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "failed",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: 1,
          durationMs: 42,
        },
      ]),
    );

    expect(projector.buildResult(buildEmptyToolTelemetry()).lastToolError).toEqual({
      toolName: "bash",
      meta: "run tests (workspace)",
      error: "fatal: missing fixture",
      mutatingAction: true,
      actionFingerprint: JSON.stringify({
        type: "commandExecution",
        command: "pnpm test extensions/codex",
        cwd: "/workspace",
      }),
    });
  });

  it("does not duplicate native tool starts when the snapshot completes a started item", async () => {
    const onAgentEvent = vi.fn();
    const trajectoryRecorder = {
      filePath: "trajectory.jsonl",
      recordEvent: vi.fn(),
      flush: vi.fn(async () => undefined),
    };
    const projector = await createProjector(
      { ...(await createParams()), onAgentEvent },
      { trajectoryRecorder },
    );
    const commandItem = {
      type: "commandExecution",
      id: "cmd-started",
      command: "pnpm test extensions/codex",
      cwd: "/workspace",
      processId: null,
      source: "agent",
      status: "completed",
      commandActions: [],
      aggregatedOutput: "ok",
      exitCode: 0,
      durationMs: 42,
    };

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: { ...commandItem, status: "inProgress", aggregatedOutput: null, exitCode: null },
      }),
    );
    await projector.handleNotification(turnCompleted([commandItem]));

    const toolEvents = onAgentEvent.mock.calls
      .map((call) => requireRecord(call[0], "agent event"))
      .filter((event) => event.stream === "tool")
      .map((event) => requireRecord(event.data, "agent event data"));
    expect(
      toolEvents.filter((event) => event.phase === "start" && event.itemId === "cmd-started"),
    ).toHaveLength(1);
    expect(
      toolEvents.filter((event) => event.phase === "result" && event.itemId === "cmd-started"),
    ).toHaveLength(1);
    expect(
      trajectoryRecorder.recordEvent.mock.calls.filter(([type]) => type === "tool.call"),
    ).toHaveLength(1);
    expect(
      trajectoryRecorder.recordEvent.mock.calls.filter(([type]) => type === "tool.result"),
    ).toHaveLength(1);
  });

  it("does not synthesize completed progress for running turn completion snapshots", async () => {
    const onAgentEvent = vi.fn();
    const projector = await createProjector({ ...(await createParams()), onAgentEvent });

    await projector.handleNotification(
      turnCompleted([
        {
          type: "commandExecution",
          id: "cmd-running-snapshot",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "inProgress",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: null,
          durationMs: null,
        },
      ]),
    );

    const toolEvents = onAgentEvent.mock.calls
      .map((call) => requireRecord(call[0], "agent event"))
      .filter((event) => event.stream === "tool")
      .map((event) => requireRecord(event.data, "agent event data"));
    expect(toolEvents).toEqual([]);
  });

  it("does not synthesize progress for stale prior-turn snapshot items", async () => {
    const onAgentEvent = vi.fn();
    const projector = await createProjector({ ...(await createParams()), onAgentEvent });

    await projector.handleNotification(
      turnCompleted([
        {
          type: "commandExecution",
          id: "cmd-prior-turn",
          turnId: "turn-old",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "ok",
          exitCode: 0,
          durationMs: 42,
        },
        {
          type: "commandExecution",
          id: "cmd-current-turn",
          turnId: TURN_ID,
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "ok",
          exitCode: 0,
          durationMs: 42,
        },
      ]),
    );

    const toolEvents = onAgentEvent.mock.calls
      .map((call) => requireRecord(call[0], "agent event"))
      .filter((event) => event.stream === "tool")
      .map((event) => requireRecord(event.data, "agent event data"));
    expect(toolEvents.map((event) => event.itemId)).toEqual([
      "cmd-current-turn",
      "cmd-current-turn",
    ]);
  });

  it("orders declined native tool diagnostics after their start event", async () => {
    const projector = await createProjector();
    const diagnosticEvents: DiagnosticEventPayload[] = [];
    const unsubscribe = onInternalDiagnosticEvent((event) => diagnosticEvents.push(event));

    try {
      await projector.handleNotification(
        forCurrentTurn("item/started", {
          item: {
            type: "commandExecution",
            id: "cmd-declined",
            command: "pnpm test extensions/codex",
            cwd: "/workspace",
            processId: null,
            source: "agent",
            status: "inProgress",
            commandActions: [],
            aggregatedOutput: null,
            exitCode: null,
            durationMs: null,
          },
        }),
      );
      await projector.handleNotification(
        forCurrentTurn("item/completed", {
          item: {
            type: "commandExecution",
            id: "cmd-declined",
            command: "pnpm test extensions/codex",
            cwd: "/workspace",
            processId: null,
            source: "agent",
            status: "declined",
            commandActions: [],
            aggregatedOutput: null,
            exitCode: null,
            durationMs: 1,
          },
        }),
      );
      await flushDiagnosticEvents();
    } finally {
      unsubscribe();
    }

    const toolDiagnosticEvents = diagnosticEvents.filter(
      (
        event,
      ): event is Extract<
        DiagnosticEventPayload,
        {
          type:
            | "tool.execution.started"
            | "tool.execution.completed"
            | "tool.execution.error"
            | "tool.execution.blocked";
        }
      > => event.type.startsWith("tool.execution."),
    );
    expect(
      toolDiagnosticEvents.map((event) => ({
        type: event.type,
        toolName: event.toolName,
        toolCallId: event.toolCallId,
      })),
    ).toEqual([
      {
        type: "tool.execution.started",
        toolName: "bash",
        toolCallId: "cmd-declined",
      },
      {
        type: "tool.execution.blocked",
        toolName: "bash",
        toolCallId: "cmd-declined",
      },
    ]);
    expect(projector.buildResult(buildEmptyToolTelemetry()).lastToolError).toEqual({
      toolName: "bash",
      meta: "run tests (workspace)",
      error: "codex native tool blocked",
      mutatingAction: true,
      actionFingerprint: JSON.stringify({
        type: "commandExecution",
        command: "pnpm test extensions/codex",
        cwd: "/workspace",
      }),
    });
  });

  it("clears a recovered declined native tool error", async () => {
    const projector = await createProjector();

    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "commandExecution",
          id: "cmd-declined",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "declined",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: null,
          durationMs: 1,
        },
      }),
    );
    expect(projector.buildResult(buildEmptyToolTelemetry()).lastToolError).toEqual({
      toolName: "bash",
      meta: "run tests (workspace)",
      error: "codex native tool blocked",
      mutatingAction: true,
      actionFingerprint: JSON.stringify({
        type: "commandExecution",
        command: "pnpm test extensions/codex",
        cwd: "/workspace",
      }),
    });

    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "commandExecution",
          id: "cmd-recovered",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "ok",
          exitCode: 0,
          durationMs: 42,
        },
      }),
    );

    expect(projector.buildResult(buildEmptyToolTelemetry()).lastToolError).toBeUndefined();
  });

  it("does not clear a declined native tool error with a different action", async () => {
    const projector = await createProjector();

    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "commandExecution",
          id: "cmd-declined",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "declined",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: null,
          durationMs: 1,
        },
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "commandExecution",
          id: "cmd-unrelated-success",
          command: "pnpm test src/foo.test.ts",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "ok",
          exitCode: 0,
          durationMs: 42,
        },
      }),
    );

    expect(projector.buildResult(buildEmptyToolTelemetry()).lastToolError).toEqual({
      toolName: "bash",
      meta: "run tests (workspace)",
      error: "codex native tool blocked",
      mutatingAction: true,
      actionFingerprint: JSON.stringify({
        type: "commandExecution",
        command: "pnpm test extensions/codex",
        cwd: "/workspace",
      }),
    });
  });

  it("emits after_tool_call observations for Codex-native tool item completions", async () => {
    const afterToolCall = vi.fn();
    initializeGlobalHookRunner(
      createMockPluginRegistry([{ hookName: "after_tool_call", handler: afterToolCall }]),
    );
    const projector = await createProjector({
      ...(await createParams()),
      agentId: "main",
      sessionKey: "agent:main:session-1",
    });

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: {
          type: "commandExecution",
          id: "cmd-observed",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "inProgress",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: null,
          durationMs: null,
        },
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "commandExecution",
          id: "cmd-observed",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "ok",
          exitCode: 0,
          durationMs: 42,
        },
      }),
    );

    await vi.waitFor(() => expect(afterToolCall).toHaveBeenCalledTimes(1));
    const event = requireRecord(
      mockCallArg(afterToolCall, 0, 0, "after_tool_call event"),
      "after_tool_call event",
    );
    expect(event.toolName).toBe("bash");
    expect(event.params).toEqual({ command: "pnpm test extensions/codex", cwd: "/workspace" });
    expect(event.runId).toBe("run-1");
    expect(event.toolCallId).toBe("cmd-observed");
    expect(event.result).toEqual({ status: "completed", exitCode: 0, durationMs: 42 });
    expect(event.durationMs).toBeGreaterThanOrEqual(42);
    const context = requireRecord(
      mockCallArg(afterToolCall, 0, 1, "after_tool_call context"),
      "after_tool_call context",
    );
    expect(context.agentId).toBe("main");
    expect(context.sessionId).toBe("session-1");
    expect(context.sessionKey).toBe("agent:main:session-1");
    expect(context.runId).toBe("run-1");
    expect(context.toolName).toBe("bash");
    expect(context.toolCallId).toBe("cmd-observed");
  });

  it("does not duplicate native items already covered by PostToolUse relay", async () => {
    const afterToolCall = vi.fn();
    initializeGlobalHookRunner(
      createMockPluginRegistry([{ hookName: "after_tool_call", handler: afterToolCall }]),
    );
    const projector = await createProjector(
      { ...(await createParams()), sessionKey: "agent:main:session-1" },
      { nativePostToolUseRelayEnabled: true },
    );

    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "commandExecution",
          id: "cmd-relayed",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "ok",
          exitCode: 0,
          durationMs: 42,
        },
      }),
    );
    expect(afterToolCall).not.toHaveBeenCalled();

    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "webSearch",
          id: "search-observed",
          query: "native tool observability",
          status: "completed",
          durationMs: 5,
        },
      }),
    );

    await vi.waitFor(() => expect(afterToolCall).toHaveBeenCalledTimes(1));
    const event = requireRecord(
      mockCallArg(afterToolCall, 0, 0, "after_tool_call event"),
      "after_tool_call event",
    );
    expect(event.toolName).toBe("web_search");
    expect(event.params).toEqual({ query: "native tool observability" });
    expect(event.runId).toBe("run-1");
    expect(event.toolCallId).toBe("search-observed");
    expect(event.result).toEqual({
      status: "completed",
      durationMs: 5,
      query: "native tool observability",
    });
  });

  it("uses Codex web search action metadata when the top-level query is empty", async () => {
    const afterToolCall = vi.fn();
    initializeGlobalHookRunner(
      createMockPluginRegistry([{ hookName: "after_tool_call", handler: afterToolCall }]),
    );
    const projector = await createProjector();

    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "webSearch",
          id: "search-observed",
          query: "",
          action: {
            type: "search",
            query: "native action query",
            queries: ["native action query", "secondary query"],
          },
          status: "completed",
          durationMs: 5,
        },
      }),
    );

    await vi.waitFor(() => expect(afterToolCall).toHaveBeenCalledTimes(1));
    const event = requireRecord(
      mockCallArg(afterToolCall, 0, 0, "after_tool_call event"),
      "after_tool_call event",
    );
    expect(event.toolName).toBe("web_search");
    expect(event.params).toEqual({
      query: "native action query",
      queries: ["native action query", "secondary query"],
    });
    expect(event.result).toEqual({
      status: "completed",
      durationMs: 5,
      query: "native action query",
      queries: ["native action query", "secondary query"],
    });
  });

  it("marks unavailable Codex web search queries explicitly", async () => {
    const afterToolCall = vi.fn();
    initializeGlobalHookRunner(
      createMockPluginRegistry([{ hookName: "after_tool_call", handler: afterToolCall }]),
    );
    const projector = await createProjector();

    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "webSearch",
          id: "search-observed",
          query: "",
          action: { type: "other" },
          status: "completed",
        },
      }),
    );

    await vi.waitFor(() => expect(afterToolCall).toHaveBeenCalledTimes(1));
    const event = requireRecord(
      mockCallArg(afterToolCall, 0, 0, "after_tool_call event"),
      "after_tool_call event",
    );
    expect(event.params).toEqual({
      action: "other",
      queryUnavailable: true,
    });
    expect(event.result).toEqual({
      status: "completed",
      action: "other",
      queryUnavailable: true,
    });
  });

  it("records dynamic OpenClaw tool calls in mirrored transcript snapshots", async () => {
    const projector = await createProjector();

    projector.recordDynamicToolCall({
      callId: "call-browser-1",
      tool: "browser",
      arguments: { action: "open", url: "http://127.0.0.1:3000" },
    });
    projector.recordDynamicToolResult({
      callId: "call-browser-1",
      tool: "browser",
      success: true,
      contentItems: [{ type: "inputText", text: "opened" }],
    });
    await projector.handleNotification(agentMessageDelta("done"));

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.messagesSnapshot.map((message) => message.role)).toEqual([
      "user",
      "assistant",
      "toolResult",
      "assistant",
    ]);
    const assistant = requireRecord(result.messagesSnapshot[1], "assistant tool call message");
    expect(assistant.role).toBe("assistant");
    expect(requireArray(assistant.content, "assistant content")[0]).toEqual({
      type: "toolCall",
      id: "call-browser-1",
      name: "browser",
      arguments: { action: "open", url: "http://127.0.0.1:3000" },
      input: { action: "open", url: "http://127.0.0.1:3000" },
    });
    const toolResultMessage = requireRecord(result.messagesSnapshot[2], "tool result message");
    expect(toolResultMessage.role).toBe("toolResult");
    expect(toolResultMessage.toolCallId).toBe("call-browser-1");
    expect(toolResultMessage.toolName).toBe("browser");
    expect(toolResultMessage.isError).toBe(false);
    const toolResultContent = requireRecord(
      requireArray(toolResultMessage.content, "tool result content")[0],
      "tool result content item",
    );
    expect(toolResultContent.type).toBe("toolResult");
    expect(toolResultContent.id).toBe("call-browser-1");
    expect(toolResultContent.name).toBe("browser");
    expect(toolResultContent.toolName).toBe("browser");
    expect(toolResultContent.toolCallId).toBe("call-browser-1");
    expect(toolResultContent.content).toBe("opened");
  });

  it("does not mirror Codex-native web searches into transcript snapshots", async () => {
    const projector = await createProjector();

    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "webSearch",
          id: "search-observed",
          status: "completed",
          durationMs: 5,
        },
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(
      result.messagesSnapshot.some((message) => {
        const record = message as unknown as Record<string, unknown>;
        if (record.role === "toolResult") {
          return true;
        }
        const content = Array.isArray(record.content) ? record.content : [];
        return content.some((entry) => {
          return (
            typeof entry === "object" &&
            entry !== null &&
            (entry as Record<string, unknown>).type === "toolCall"
          );
        });
      }),
    ).toBe(false);
  });

  it("carries async-started dynamic tool metadata into attempt results", async () => {
    const projector = await createProjector();

    projector.recordDynamicToolCall({
      callId: "call-image-1",
      tool: "image_generate",
      arguments: { action: "generate", prompt: "lighthouse" },
    });
    projector.recordDynamicToolResult({
      callId: "call-image-1",
      tool: "image_generate",
      asyncStarted: true,
      success: true,
      sideEffectEvidence: true,
      contentItems: [{ type: "inputText", text: "Background task started." }],
    });
    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "dynamicToolCall",
          id: "call-image-1",
          namespace: null,
          tool: "image_generate",
          arguments: { action: "generate", prompt: "lighthouse" },
          status: "completed",
          contentItems: [{ type: "inputText", text: "Background task started." }],
          success: true,
          durationMs: 10,
        },
      }),
    );

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.toolMetas).toEqual([
      {
        toolName: "image_generate",
        meta: "lighthouse",
        asyncStarted: true,
      },
    ]);
    expect(result.replayMetadata).toEqual({
      hadPotentialSideEffects: true,
      replaySafe: false,
    });
  });

  it("emits verbose summaries for transcript-recorded dynamic tool calls", async () => {
    const onAgentEvent = vi.fn();
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      onAgentEvent,
      onToolResult,
    });

    projector.recordDynamicToolCall({
      callId: "call-browser-1",
      tool: "browser",
      arguments: { action: "open", url: "http://127.0.0.1:3000" },
    });

    const toolEvents = onAgentEvent.mock.calls.filter(([event]) => {
      const record = requireRecord(event, "agent event");
      return record.stream === "tool";
    });
    expect(toolEvents).toHaveLength(0);
    expect(onToolResult).toHaveBeenCalledTimes(1);
    const payload = mockCallArg(onToolResult, 0, 0, "onToolResult") as { text?: string };
    expect(payload.text).toContain("Browser");
  });

  it("does not replay transcript summaries when only tool output is enabled", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      onToolResult,
      shouldEmitToolResult: () => false,
      shouldEmitToolOutput: () => true,
    });

    projector.recordDynamicToolCall({
      callId: "call-browser-1",
      tool: "browser",
      arguments: { action: "open", url: "http://127.0.0.1:3000" },
    });
    projector.recordDynamicToolResult({
      callId: "call-browser-1",
      tool: "browser",
      success: true,
      contentItems: [{ type: "inputText", text: "opened" }],
    });

    expect(onToolResult).toHaveBeenCalledTimes(1);
    const payload = mockCallArg(onToolResult, 0, 0, "onToolResult") as { text?: string };
    expect(payload.text).toContain("opened");
    expect(payload.text).toContain("```txt\nopened\n```");
  });

  it("keeps side-effect evidence for dynamic tools that error after execution", async () => {
    const projector = await createProjector();

    projector.recordDynamicToolCall({
      callId: "call-process-kill",
      tool: "process",
      arguments: { action: "kill", sessionId: "session-1" },
    });
    projector.recordDynamicToolResult({
      callId: "call-process-kill",
      tool: "process",
      success: false,
      terminalType: "error",
      sideEffectEvidence: true,
      contentItems: [{ type: "inputText", text: "process exited" }],
    });

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.replayMetadata).toEqual({ hadPotentialSideEffects: true, replaySafe: false });
  });

  it("does not keep side-effect evidence for pre-execution dynamic tool errors", async () => {
    const projector = await createProjector();

    projector.recordDynamicToolCall({
      callId: "call-unknown-message",
      tool: "message",
      arguments: { action: "send", text: "hello" },
    });
    projector.recordDynamicToolResult({
      callId: "call-unknown-message",
      tool: "message",
      success: false,
      terminalType: "error",
      contentItems: [{ type: "inputText", text: "Unknown OpenClaw tool: message" }],
    });

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.replayMetadata).toEqual({ hadPotentialSideEffects: false, replaySafe: true });
  });

  it("does not mark blocked dynamic tools as side-effecting", async () => {
    const projector = await createProjector();

    projector.recordDynamicToolCall({
      callId: "call-bash-blocked",
      tool: "bash",
      arguments: { command: "touch blocked.txt" },
    });
    projector.recordDynamicToolResult({
      callId: "call-bash-blocked",
      tool: "bash",
      success: false,
      terminalType: "blocked",
      sideEffectEvidence: true,
      contentItems: [{ type: "inputText", text: "blocked" }],
    });

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.replayMetadata).toEqual({ hadPotentialSideEffects: false, replaySafe: true });
  });

  it("treats completed native MCP tool calls as side-effect evidence", async () => {
    const projector = await createProjector();

    await projector.handleNotification({
      method: "item/completed",
      params: {
        threadId: "thread-1",
        turnId: "turn-1",
        item: {
          id: "mcp-1",
          type: "mcpToolCall",
          server: "github",
          tool: "create_issue",
          status: "completed",
          arguments: { title: "check replay safety" },
        },
      },
    });

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(result.replayMetadata).toEqual({ hadPotentialSideEffects: true, replaySafe: false });
  });

  it("suppresses transcript progress for message-like tools", async () => {
    const onAgentEvent = vi.fn();
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      onAgentEvent,
      onToolResult,
    });

    projector.recordDynamicToolCall({
      callId: "call-message-1",
      tool: "message",
      arguments: { action: "send", text: "hello" },
    });
    projector.recordDynamicToolResult({
      callId: "call-message-1",
      tool: "message",
      success: true,
      contentItems: [{ type: "inputText", text: "sent" }],
    });

    const toolEvents = onAgentEvent.mock.calls.filter(([event]) => {
      const record = requireRecord(event, "agent event");
      return record.stream === "tool";
    });
    expect(toolEvents).toHaveLength(0);
    expect(onToolResult).not.toHaveBeenCalled();
  });

  it("does not parse shell command text to suppress transcript progress", async () => {
    const onAgentEvent = vi.fn();
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      onAgentEvent,
      onToolResult,
    });

    projector.recordDynamicToolCall({
      callId: "call-log-activity-1",
      tool: "bash",
      arguments: {
        command:
          '/bin/bash -lc \'/home/openclaw/.openclaw/workspace/bin/log_activity.sh "web_search" "Grilled salmon research"\'',
        cwd: "/workspace",
      },
    });
    projector.recordDynamicToolResult({
      callId: "call-log-activity-1",
      tool: "bash",
      success: true,
      contentItems: [{ type: "inputText", text: "Logged: [web_search] Grilled salmon research" }],
    });

    expect(onAgentEvent).not.toHaveBeenCalled();
    const toolProgressText = onToolResult.mock.calls
      .map(([payload]) => (payload as { text?: string }).text ?? "")
      .join("\n");
    expect(toolProgressText).toContain("log_activity.sh");

    const result = projector.buildResult(buildEmptyToolTelemetry());
    expect(result.messagesSnapshot.some((message) => message.role === "toolResult")).toBe(true);
  });

  it("keeps diagnostics for exact message-like native tool items while suppressing progress", async () => {
    const onAgentEvent = vi.fn();
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      onAgentEvent,
      onToolResult,
    });
    const diagnosticEvents: DiagnosticEventPayload[] = [];
    const unsubscribe = onInternalDiagnosticEvent((event) => diagnosticEvents.push(event));

    try {
      await projector.handleNotification(
        forCurrentTurn("item/started", {
          item: {
            type: "mcpToolCall",
            id: "mcp-message-1",
            server: null,
            tool: "message",
            arguments: { text: "hello" },
            status: "inProgress",
            result: null,
            error: null,
            durationMs: null,
          },
        }),
      );
      await projector.handleNotification(
        forCurrentTurn("item/completed", {
          item: {
            type: "mcpToolCall",
            id: "mcp-message-1",
            server: null,
            tool: "message",
            arguments: { text: "hello" },
            status: "completed",
            result: { ok: true },
            error: null,
            durationMs: 7,
          },
        }),
      );
      await flushDiagnosticEvents();
    } finally {
      unsubscribe();
    }

    const toolEvents = onAgentEvent.mock.calls.filter(([event]) => {
      const record = requireRecord(event, "agent event");
      return record.stream === "tool";
    });
    expect(toolEvents).toHaveLength(0);
    expect(onToolResult).not.toHaveBeenCalled();

    const toolDiagnosticEvents = diagnosticEvents.filter(
      (
        event,
      ): event is Extract<
        DiagnosticEventPayload,
        {
          type:
            | "tool.execution.started"
            | "tool.execution.completed"
            | "tool.execution.error"
            | "tool.execution.blocked";
        }
      > => event.type.startsWith("tool.execution."),
    );
    expect(
      toolDiagnosticEvents.map((event) => ({
        type: event.type,
        toolName: event.toolName,
        toolCallId: event.toolCallId,
        durationMs: "durationMs" in event ? event.durationMs : undefined,
      })),
    ).toEqual([
      {
        type: "tool.execution.started",
        toolName: "message",
        toolCallId: "mcp-message-1",
        durationMs: undefined,
      },
      {
        type: "tool.execution.completed",
        toolName: "message",
        toolCallId: "mcp-message-1",
        durationMs: 7,
      },
    ]);
  });

  it("does not suppress qualified external tools that end with message-like names", async () => {
    const onAgentEvent = vi.fn();
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      onAgentEvent,
      onToolResult,
    });

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: {
          type: "mcpToolCall",
          id: "mcp-email-send-1",
          server: "email",
          tool: "send",
          arguments: { to: "user@example.com" },
          status: "inProgress",
          result: null,
          error: null,
          durationMs: null,
        },
      }),
    );

    const toolStart = findAgentEvent(onAgentEvent, {
      stream: "tool",
      phase: "start",
      itemId: "mcp-email-send-1",
      name: "email.send",
    }).data;
    expect(toolStart.toolCallId).toBe("mcp-email-send-1");
    expect(onToolResult).toHaveBeenCalledWith({
      text: "🧩 Email.send: `user@example.com`",
    });
  });

  it("marks declined Codex-native tool results as non-success", async () => {
    const onAgentEvent = vi.fn();
    const projector = await createProjector({ ...(await createParams()), onAgentEvent });

    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: {
          type: "commandExecution",
          id: "cmd-declined",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "declined",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: null,
          durationMs: null,
        },
      }),
    );

    const itemEnd = findAgentEvent(onAgentEvent, {
      stream: "item",
      phase: "end",
      itemId: "cmd-declined",
    }).data;
    expect(itemEnd.kind).toBe("command");
    expect(itemEnd.name).toBe("bash");
    expect(itemEnd.status).toBe("blocked");
    expect(itemEnd.suppressChannelProgress).toBe(true);
    const toolResult = findAgentEvent(onAgentEvent, {
      stream: "tool",
      phase: "result",
      itemId: "cmd-declined",
      name: "bash",
    }).data;
    expect(toolResult.toolCallId).toBe("cmd-declined");
    expect(toolResult.status).toBe("blocked");
    expect(toolResult.isError).toBe(true);
  });

  it("leaves Codex dynamic tool item progress to item/tool/call normalization", async () => {
    const onAgentEvent = vi.fn();
    const projector = await createProjector({ ...(await createParams()), onAgentEvent });

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: {
          type: "dynamicToolCall",
          id: "call-1",
          namespace: null,
          tool: "message",
          arguments: { action: "send" },
          status: "inProgress",
          contentItems: null,
          success: null,
          durationMs: null,
        },
      }),
    );

    const itemStart = findAgentEvent(onAgentEvent, {
      stream: "item",
      phase: "start",
      name: "message",
    }).data;
    expect(itemStart.kind).toBe("tool");
    expect(itemStart.suppressChannelProgress).toBe(true);
    const calls = (onAgentEvent as { mock: { calls: unknown[][] } }).mock.calls;
    const toolStart = calls.some((call) => {
      const event = requireRecord(call[0], "agent event");
      if (event.stream !== "tool") {
        return false;
      }
      const data = requireRecord(event.data, "agent event data");
      return data.phase === "start" && data.name === "message";
    });
    expect(toolStart).toBe(false);
  });

  it("emits verbose tool summaries through onToolResult", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      onToolResult,
    });

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: {
          type: "commandExecution",
          id: "cmd-1",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "inProgress",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: null,
          durationMs: null,
        },
      }),
    );

    expect(onToolResult).toHaveBeenCalledTimes(1);
    expect(onToolResult).toHaveBeenCalledWith({
      text: "🛠️ `run tests (workspace)`",
    });
  });

  it("can emit raw verbose tool summaries through onToolResult", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      toolProgressDetail: "raw",
      onToolResult,
    });

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: {
          type: "commandExecution",
          id: "cmd-1",
          command: "pnpm test extensions/codex",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "inProgress",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: null,
          durationMs: null,
        },
      }),
    );

    expect(onToolResult).toHaveBeenCalledWith({
      text: "🛠️ `` run tests (workspace), `pnpm test extensions/codex` ``",
    });
  });

  it("redacts secrets in verbose command summaries", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      toolProgressDetail: "raw",
      onToolResult,
    });

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: {
          type: "commandExecution",
          id: "cmd-1",
          command: "OPENAI_API_KEY=sk-1234567890abcdefZZZZ pnpm test",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "inProgress",
          commandActions: [],
          aggregatedOutput: null,
          exitCode: null,
          durationMs: null,
        },
      }),
    );

    const text = (mockCallArg(onToolResult, 0, 0, "onToolResult") as { text?: string }).text;
    expect(text).toContain("OPENAI_API_KEY=*** pnpm test");
    expect(text).not.toContain("sk-1234567890abcdefZZZZ");
  });

  it("uses argument details instead of lifecycle status in verbose tool summaries", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "on",
      onToolResult,
    });

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: {
          type: "dynamicToolCall",
          id: "tool-1",
          namespace: null,
          tool: "lcm_grep",
          arguments: { query: "inProgress text" },
          status: "inProgress",
          contentItems: null,
          success: null,
          durationMs: null,
        },
      }),
    );

    expect(onToolResult).toHaveBeenCalledTimes(1);
    expect(onToolResult).toHaveBeenCalledWith({
      text: "🧩 Lcm Grep: `inProgress text`",
    });
  });

  it("emits completed tool output only when verbose full is enabled", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "full",
      onToolResult,
    });

    await projector.handleNotification(
      turnCompleted([
        {
          type: "dynamicToolCall",
          id: "tool-1",
          namespace: null,
          tool: "read",
          arguments: { path: "README.md" },
          status: "completed",
          contentItems: [{ type: "inputText", text: "file contents" }],
          success: true,
          durationMs: 12,
        },
      ]),
    );

    expect(onToolResult).toHaveBeenCalledTimes(2);
    expect(onToolResult).toHaveBeenNthCalledWith(1, {
      text: "📖 Read: `from README.md`",
    });
    expect(onToolResult).toHaveBeenNthCalledWith(2, {
      text: "📖 Read: `from README.md`\n```txt\nfile contents\n```",
    });
  });

  it("marks failed completed tool output as error progress", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "full",
      onToolResult,
    });

    await projector.handleNotification(
      turnCompleted([
        {
          type: "dynamicToolCall",
          id: "tool-1",
          namespace: null,
          tool: "bash",
          arguments: { command: "ls /tmp/missing" },
          status: "failed",
          contentItems: [{ type: "inputText", text: "No such file or directory" }],
          success: false,
          durationMs: 12,
        },
      ]),
    );

    expect(onToolResult).toHaveBeenNthCalledWith(2, {
      text: "🛠️ `list files in /tmp/missing`\n```txt\nNo such file or directory\n```",
      isError: true,
    });
  });

  it("uses a safe markdown fence for verbose tool output", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "full",
      onToolResult,
    });

    await projector.handleNotification(
      turnCompleted([
        {
          type: "dynamicToolCall",
          id: "tool-1",
          namespace: null,
          tool: "read",
          arguments: { path: "README.md" },
          status: "completed",
          contentItems: [{ type: "inputText", text: "line\n```\nMEDIA:/tmp/secret.png" }],
          success: true,
          durationMs: 12,
        },
      ]),
    );

    expect(onToolResult).toHaveBeenNthCalledWith(2, {
      text: "📖 Read: `from README.md`\n````txt\nline\n```\nMEDIA:/tmp/secret.png\n````",
    });
  });

  it("bounds streamed verbose tool output", async () => {
    const onToolResult = vi.fn();
    const projector = await createProjector({
      ...(await createParams()),
      verboseLevel: "full",
      onToolResult,
    });

    for (let i = 0; i < 25; i += 1) {
      await projector.handleNotification(
        forCurrentTurn("item/commandExecution/outputDelta", {
          itemId: "cmd-1",
          delta: `line ${i}\n`,
        }),
      );
    }
    await projector.handleNotification(
      turnCompleted([
        {
          type: "commandExecution",
          id: "cmd-1",
          command: "pnpm test",
          cwd: "/workspace",
          processId: null,
          source: "agent",
          status: "completed",
          commandActions: [],
          aggregatedOutput: "final output should not duplicate streamed output",
          exitCode: 0,
          durationMs: 12,
        },
      ]),
    );

    expect(onToolResult).toHaveBeenCalledTimes(21);
    const truncatedOutput = mockCallArg(onToolResult, 19, 0, "onToolResult") as {
      text?: string;
    };
    expect(truncatedOutput.text).toContain("...(truncated)...");
    expect(JSON.stringify(onToolResult.mock.calls)).not.toContain(
      "final output should not duplicate",
    );
  });

  it("continues projecting turn completion when an event consumer throws", async () => {
    const onAgentEvent = vi.fn(() => {
      throw new Error("consumer failed");
    });
    const projector = await createProjector({
      ...(await createParams()),
      onAgentEvent,
    });

    await expect(
      projector.handleNotification(
        turnCompleted([
          { type: "plan", id: "plan-1", text: "step one\nstep two" },
          { type: "agentMessage", id: "msg-1", text: "final answer" },
        ]),
      ),
    ).resolves.toBeUndefined();

    const result = projector.buildResult(buildEmptyToolTelemetry());

    expect(findAgentEvent(onAgentEvent, { stream: "plan" }).data.steps).toEqual([
      "step one",
      "step two",
    ]);
    expect(result.assistantTexts).toEqual(["final answer"]);
    expect(JSON.stringify(result.messagesSnapshot)).toContain("Codex plan");
  });

  it("fires before_compaction and after_compaction hooks for codex compaction items", async () => {
    const { projector, beforeCompaction, afterCompaction } = await createProjectorWithHooks();
    const openSpy = vi.spyOn(SessionManager, "open");

    await projector.handleNotification(
      forCurrentTurn("item/started", {
        item: { type: "contextCompaction", id: "compact-1" },
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("item/completed", {
        item: { type: "contextCompaction", id: "compact-1" },
      }),
    );
    expect(openSpy).not.toHaveBeenCalled();

    const beforePayload = requireRecord(
      mockCallArg(beforeCompaction, 0, 0, "beforeCompaction"),
      "before payload",
    );
    expect(beforePayload.messageCount).toBe(1);
    expect(String(beforePayload.sessionFile)).toContain("session.jsonl");
    const beforeMessages = requireArray(beforePayload.messages, "before messages");
    expect(requireRecord(beforeMessages[0], "before message").role).toBe("assistant");
    const beforeContext = requireRecord(
      mockCallArg(beforeCompaction, 0, 1, "beforeCompaction"),
      "before context",
    );
    expect(beforeContext.runId).toBe("run-1");
    expect(beforeContext.sessionId).toBe("session-1");
    const afterPayload = requireRecord(
      mockCallArg(afterCompaction, 0, 0, "afterCompaction"),
      "after payload",
    );
    expect(afterPayload.messageCount).toBe(1);
    expect(afterPayload.compactedCount).toBe(-1);
    expect(String(afterPayload.sessionFile)).toContain("session.jsonl");
    const afterContext = requireRecord(
      mockCallArg(afterCompaction, 0, 1, "afterCompaction"),
      "after context",
    );
    expect(afterContext.runId).toBe("run-1");
    expect(afterContext.sessionId).toBe("session-1");
  });

  it("projects codex hook started and completed notifications into agent events", async () => {
    const onAgentEvent = vi.fn();
    const params = await createParams();
    const projector = await createProjector({ ...params, onAgentEvent });

    await projector.handleNotification(
      forCurrentTurn("hook/started", {
        run: {
          id: "hook-1",
          eventName: "preToolUse",
          handlerType: "command",
          executionMode: "sync",
          scope: "turn",
          source: "project",
          sourcePath: "/repo/.codex/hooks.json",
          status: "running",
          statusMessage: null,
          entries: [],
        },
      }),
    );
    await projector.handleNotification(
      forCurrentTurn("hook/completed", {
        run: {
          id: "hook-1",
          eventName: "preToolUse",
          handlerType: "command",
          executionMode: "sync",
          scope: "turn",
          source: "project",
          sourcePath: "/repo/.codex/hooks.json",
          status: "blocked",
          statusMessage: "blocked by hook",
          durationMs: 42,
          entries: [{ kind: "stderr", text: "blocked" }],
        },
      }),
    );

    const started = findAgentEvent(onAgentEvent, {
      stream: "codex_app_server.hook",
      phase: "started",
    }).data;
    expect(started.threadId).toBe(THREAD_ID);
    expect(started.turnId).toBe(TURN_ID);
    expect(started.hookRunId).toBe("hook-1");
    expect(started.eventName).toBe("preToolUse");
    expect(started.status).toBe("running");
    const completed = findAgentEvent(onAgentEvent, {
      stream: "codex_app_server.hook",
      phase: "completed",
    }).data;
    expect(completed.hookRunId).toBe("hook-1");
    expect(completed.status).toBe("blocked");
    expect(completed.statusMessage).toBe("blocked by hook");
    expect(completed.durationMs).toBe(42);
    expect(completed.entries).toEqual([{ kind: "stderr", text: "blocked" }]);
  });

  it("projects thread-scoped codex hook notifications that omit a turn id", async () => {
    const onAgentEvent = vi.fn();
    const params = await createParams();
    const projector = await createProjector({ ...params, onAgentEvent });

    await projector.handleNotification({
      method: "hook/started",
      params: {
        threadId: THREAD_ID,
        turnId: null,
        run: {
          id: "hook-thread-1",
          eventName: "sessionStart",
          handlerType: "command",
          executionMode: "sync",
          scope: "thread",
          source: "project",
          sourcePath: "/repo/.codex/hooks.json",
          status: "running",
          statusMessage: null,
          entries: [],
        },
      },
    });

    const started = findAgentEvent(onAgentEvent, {
      stream: "codex_app_server.hook",
      phase: "started",
    }).data;
    expect(started.threadId).toBe(THREAD_ID);
    expect(started.turnId).toBeNull();
    expect(started.hookRunId).toBe("hook-thread-1");
    expect(started.eventName).toBe("sessionStart");
    expect(started.scope).toBe("thread");
  });
});
