/** Tests live model switching behavior in active agent command sessions. */
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { SessionEntry } from "../config/sessions.js";
import { INTERNAL_RUNTIME_CONTEXT_BEGIN, INTERNAL_RUNTIME_CONTEXT_END } from "./internal-events.js";
import { LiveSessionModelSwitchError } from "./live-model-switch-error.js";
import {
  createAgentRunDirectAbortError,
  createAgentRunRestartAbortError,
} from "./run-termination.js";

const state = vi.hoisted(() => ({
  defaultRuntimeConfig: {
    agents: {
      defaults: {
        models: {
          "anthropic/claude": {},
          "openai/claude": {},
          "openai/gpt-5.4": {},
        },
      },
    },
  },
  runtimeConfigMock: undefined as unknown,
  acpResolveSessionMock: vi.fn((..._args: unknown[]): unknown => null),
  acpRunTurnMock: vi.fn((..._args: unknown[]): unknown => undefined),
  buildAcpResultMock: vi.fn(),
  createAcpVisibleTextAccumulatorMock: vi.fn(),
  emitAcpLifecycleEndMock: vi.fn(),
  emitAcpLifecycleErrorMock: vi.fn(),
  persistCliTurnTranscriptMock: vi.fn(),
  persistAcpTurnTranscriptMock: vi.fn(),
  runCliTurnCompactionLifecycleMock: vi.fn(),
  resolveAcpAgentPolicyErrorMock: vi.fn(),
  resolveAcpDispatchPolicyErrorMock: vi.fn(),
  resolveAcpExplicitTurnPolicyErrorMock: vi.fn(),
  runWithModelFallbackMock: vi.fn(),
  runAgentAttemptMock: vi.fn(),
  resolveAgentSkillsFilterMock: vi.fn(
    (_cfg?: unknown, _agentId?: string): string[] | undefined => undefined,
  ),
  resolveEffectiveModelFallbacksMock: vi.fn().mockReturnValue(undefined),
  hasLegacyAutoFallbackWithoutOriginMock: vi.fn((_entry: unknown) => false),
  applyModelOverrideToSessionEntryMock: vi.fn((_params: unknown) => ({ updated: false })),
  resolveAutoFallbackPrimaryProbeMock: vi.fn((_params: unknown) => undefined as unknown),
  resolveChannelModelOverrideMock: vi.fn((_params: unknown) => null as unknown),
  assertLifecycleCurrentMock: vi.fn(),
  emitAgentEventMock: vi.fn(),
  registerAgentRunContextMock: vi.fn(),
  clearAgentRunContextMock: vi.fn(),
  updateSessionStoreAfterAgentRunMock: vi.fn(),
  deliverAgentCommandResultMock: vi.fn(),
  resolveAgentDeliveryPlanMock: vi.fn(),
  resolveAgentOutboundTargetMock: vi.fn(),
  resolveMessageChannelSelectionMock: vi.fn(),
  trajectoryRecordEventMock: vi.fn(),
  trajectoryFlushMock: vi.fn(async () => undefined),
  persistSessionEntryMock: vi.fn(async (..._args: unknown[]): Promise<unknown> => undefined),
  clearSessionAuthProfileOverrideMock: vi.fn(),
  isThinkingLevelSupportedMock: vi.fn((_args: unknown) => true),
  resolveSupportedThinkingLevelMock: vi.fn(({ level }: { level?: string }) => level),
  resolveThinkingDefaultMock: vi.fn((_args: unknown) => "low"),
  loadManifestModelCatalogMock: vi.fn(() => []),
  buildWorkspaceSkillSnapshotMock: vi.fn((..._args: unknown[]): unknown => ({
    prompt: "",
    skills: [],
    resolvedSkills: [],
    version: 0,
  })),
  prepareInternalSessionEffectsTranscriptMock: vi.fn(),
  removeInternalSessionEffectsTranscriptMock: vi.fn(),
  authProfileStoreMock: { profiles: {} } as { profiles: Record<string, unknown> },
  sessionEntryMock: undefined as SessionEntry | undefined,
  sessionStoreMock: undefined as unknown,
  storePathMock: undefined as string | undefined,
  resolvedSessionKeyMock: undefined as string | undefined,
}));

vi.mock("./model-fallback.js", () => ({
  runWithModelFallback: (params: unknown) => state.runWithModelFallbackMock(params),
}));

vi.mock("./command/attempt-execution.runtime.js", () => ({
  buildAcpResult: (...args: unknown[]) => state.buildAcpResultMock(...args),
  createAcpVisibleTextAccumulator: () => state.createAcpVisibleTextAccumulatorMock(),
  emitAcpAssistantDelta: vi.fn(),
  emitAcpLifecycleEnd: (...args: unknown[]) => state.emitAcpLifecycleEndMock(...args),
  emitAcpLifecycleError: (...args: unknown[]) => state.emitAcpLifecycleErrorMock(...args),
  emitAcpLifecycleStart: vi.fn(),
  emitAcpRuntimeEvent: vi.fn(),
  persistCliTurnTranscript: (...args: unknown[]) => state.persistCliTurnTranscriptMock(...args),
  persistAcpTurnTranscript: (...args: unknown[]) => state.persistAcpTurnTranscriptMock(...args),
  persistSessionEntry: vi.fn(),
  prependInternalEventContext: (body: string) => body,
  runAgentAttempt: (...args: unknown[]) => state.runAgentAttemptMock(...args),
  sessionFileHasContent: vi.fn(async () => false),
}));

vi.mock("./command/attempt-execution.shared.js", async () => {
  const actual = await vi.importActual<typeof import("./command/attempt-execution.shared.js")>(
    "./command/attempt-execution.shared.js",
  );
  return {
    ...actual,
    persistSessionEntry: (...args: unknown[]) => state.persistSessionEntryMock(...args),
  };
});

vi.mock("./command/delivery.runtime.js", () => ({
  deliverAgentCommandResult: (...args: unknown[]) => state.deliverAgentCommandResultMock(...args),
}));

vi.mock("./command/cli-compaction.js", () => ({
  runCliTurnCompactionLifecycle: (...args: unknown[]) =>
    state.runCliTurnCompactionLifecycleMock(...args),
}));

vi.mock("./command/run-context.js", () => ({
  resolveAgentRunContext: (opts: {
    accountId?: string;
    channel?: string;
    groupChannel?: string | null;
    groupId?: string | null;
    groupSpace?: string | null;
    messageChannel?: string;
    replyChannel?: string;
    runContext?: {
      accountId?: string;
      currentChannelId?: string;
      currentThreadTs?: string;
      groupChannel?: string | null;
      groupId?: string | null;
      groupSpace?: string | null;
      messageChannel?: string;
      replyToMode?: "off" | "first" | "all" | "batched";
    };
    threadId?: string | number;
    to?: string;
  }) => ({
    messageChannel:
      opts.runContext?.messageChannel ?? opts.messageChannel ?? opts.replyChannel ?? opts.channel,
    accountId: opts.runContext?.accountId ?? opts.accountId ?? "acct",
    groupId: opts.runContext?.groupId ?? opts.groupId,
    groupChannel: opts.runContext?.groupChannel ?? opts.groupChannel,
    groupSpace: opts.runContext?.groupSpace ?? opts.groupSpace,
    currentChannelId: undefined,
    currentThreadTs:
      opts.runContext?.currentThreadTs ??
      (opts.threadId == null ? undefined : String(opts.threadId)),
    replyToMode: opts.runContext?.replyToMode,
    hasRepliedRef: { current: false },
  }),
}));

vi.mock("./command/session-store.runtime.js", () => ({
  loadSessionEntry: ({ sessionKey }: { sessionKey: string }) =>
    (state.sessionStoreMock as Record<string, SessionEntry> | undefined)?.[sessionKey],
  updateSessionStoreAfterAgentRun: (...args: unknown[]) =>
    state.updateSessionStoreAfterAgentRunMock(...args),
}));

vi.mock("./command/session.js", () => ({
  resolveSession: () => {
    const sessionEntry: SessionEntry = state.sessionEntryMock ?? {
      sessionId: "session-1",
      updatedAt: Date.now(),
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    return {
      sessionId: "session-1",
      sessionKey: state.resolvedSessionKeyMock ?? "agent:main:main",
      sessionEntry,
      sessionStore: state.sessionStoreMock,
      storePath: state.storePathMock,
      isNewSession: false,
      persistedThinking:
        typeof sessionEntry.thinkingLevel === "string" ? sessionEntry.thinkingLevel : undefined,
      persistedVerbose: undefined,
    };
  },
}));

vi.mock("./command/types.js", () => ({}));

vi.mock("./harness/runtime-plugin.js", () => ({
  ensureSelectedAgentHarnessPlugin: vi.fn(async () => undefined),
}));

vi.mock("../acp/policy.js", () => ({
  isAcpEnabledByPolicy: () => true,
  resolveAcpAgentPolicyError: (...args: unknown[]) => state.resolveAcpAgentPolicyErrorMock(...args),
  resolveAcpDispatchPolicyError: (...args: unknown[]) =>
    state.resolveAcpDispatchPolicyErrorMock(...args),
  resolveAcpExplicitTurnPolicyError: (...args: unknown[]) =>
    state.resolveAcpExplicitTurnPolicyErrorMock(...args),
}));

vi.mock("../acp/runtime/errors.js", () => ({
  toAcpRuntimeError: ({ error }: { error: unknown }) =>
    error instanceof Error ? error : new Error(String(error)),
}));

vi.mock("@openclaw/acp-core/runtime/session-identifiers", () => ({
  resolveAcpSessionCwd: () => "/tmp",
}));

vi.mock("../auto-reply/thinking.js", () => ({
  formatThinkingLevels: () => "low, medium, high",
  normalizeThinkLevel: (v?: string) => v || undefined,
  normalizeVerboseLevel: (v?: string) => v || undefined,
  isThinkingLevelSupported: (args: unknown) => state.isThinkingLevelSupportedMock(args),
  resolveSupportedThinkingLevel: (args: { level?: string }) =>
    state.resolveSupportedThinkingLevelMock(args),
  supportsXHighThinking: () => false,
}));

vi.mock("../cli/command-format.js", () => ({
  formatCliCommand: (cmd: string) => cmd,
}));

vi.mock("../cli/command-secret-gateway.js", () => ({
  resolveCommandSecretRefsViaGateway: async (params: { config: unknown }) => ({
    resolvedConfig: params.config,
    diagnostics: [],
  }),
}));

vi.mock("../cli/command-secret-targets.js", () => ({
  getAgentRuntimeCommandSecretTargetIds: () => [],
}));

vi.mock("../cli/deps.js", () => ({
  createDefaultDeps: () => ({}),
}));

vi.mock("../config/io.js", () => ({
  getRuntimeConfig: () => state.runtimeConfigMock ?? state.defaultRuntimeConfig,
  readConfigFileSnapshotForWrite: async () => ({
    snapshot: { valid: false },
  }),
}));

vi.mock("./agent-runtime-config.js", () => {
  return {
    resolveAgentRuntimeConfig: async () => ({
      loadedRaw: state.runtimeConfigMock ?? state.defaultRuntimeConfig,
      sourceConfig: state.runtimeConfigMock ?? state.defaultRuntimeConfig,
      cfg: state.runtimeConfigMock ?? state.defaultRuntimeConfig,
    }),
  };
});

vi.mock("../config/runtime-snapshot.js", () => ({
  setRuntimeConfigSnapshot: vi.fn(),
}));

vi.mock("../config/sessions.js", () => ({
  resolveAgentIdFromSessionKey: () => "default",
  mergeSessionEntry: (a: unknown, b: unknown) => ({ ...(a as object), ...(b as object) }),
  updateSessionStore: vi.fn(
    async (_path: string, fn: (store: Record<string, unknown>) => unknown) => {
      const store: Record<string, unknown> = {};
      return fn(store);
    },
  ),
}));

vi.mock("../config/sessions/transcript-resolve.runtime.js", () => ({
  resolveSessionTranscriptFile: async () => ({
    sessionFile: "/tmp/session.jsonl",
    sessionEntry: { sessionId: "session-1", updatedAt: Date.now() },
  }),
}));

vi.mock("./internal-session-effects.js", () => ({
  prepareInternalSessionEffectsTranscript: (...args: unknown[]) =>
    state.prepareInternalSessionEffectsTranscriptMock(...args),
  removeInternalSessionEffectsTranscript: (...args: unknown[]) =>
    state.removeInternalSessionEffectsTranscriptMock(...args),
}));

vi.mock("../infra/agent-events.js", () => ({
  assertAgentRunLifecycleGenerationCurrent: (...args: unknown[]) =>
    state.assertLifecycleCurrentMock(...args),
  captureAgentRunLifecycleGeneration: () => "test-generation",
  clearAgentRunContext: (...args: unknown[]) => state.clearAgentRunContextMock(...args),
  emitAgentEvent: (...args: unknown[]) => state.emitAgentEventMock(...args),
  getAgentEventLifecycleGeneration: () => "test-generation",
  onAgentEvent: vi.fn(),
  registerAgentRunContext: (...args: unknown[]) => state.registerAgentRunContextMock(...args),
  withAgentRunLifecycleGeneration: (_generation: string, run: () => unknown) => run(),
}));

vi.mock("../infra/outbound/session-context.js", () => ({
  buildOutboundSessionContext: () => ({}),
}));

vi.mock("../infra/outbound/agent-delivery.js", () => ({
  resolveAgentDeliveryPlan: (...args: unknown[]) => state.resolveAgentDeliveryPlanMock(...args),
  resolveAgentOutboundTarget: (...args: unknown[]) => state.resolveAgentOutboundTargetMock(...args),
}));

vi.mock("../infra/outbound/channel-selection.js", () => ({
  resolveMessageChannelSelection: (...args: unknown[]) =>
    state.resolveMessageChannelSelectionMock(...args),
}));

vi.mock("../infra/skills-remote.js", () => ({
  getRemoteSkillEligibility: () => ({ eligible: false }),
}));

vi.mock("../logging/subsystem.js", () => ({
  createSubsystemLogger: () => {
    const logger = {
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
      debug: vi.fn(),
      trace: vi.fn(),
      raw: vi.fn(),
      child: vi.fn(() => logger),
    };
    return logger;
  },
}));

vi.mock("../channels/model-overrides.js", () => ({
  resolveChannelModelOverride: (params: unknown) => state.resolveChannelModelOverrideMock(params),
}));

vi.mock("../routing/session-key.js", async () => {
  const actual = await vi.importActual<typeof import("../routing/session-key.js")>(
    "../routing/session-key.js",
  );
  return {
    ...actual,
    normalizeAgentId: (id: string) => id,
    normalizeMainKey: (key?: string | null) => key?.trim() || "main",
  };
});

vi.mock("../runtime.js", () => ({
  defaultRuntime: {
    error: vi.fn(),
    log: vi.fn(),
  },
}));

vi.mock("../sessions/level-overrides.js", () => ({
  applyVerboseOverride: vi.fn(),
}));

vi.mock("../sessions/model-overrides.js", () => ({
  applyModelOverrideToSessionEntry: (params: unknown) =>
    state.applyModelOverrideToSessionEntryMock(params),
  repairProviderWrappedModelOverride: () => ({ updated: false }),
}));

vi.mock("../sessions/send-policy.js", () => ({
  resolveSendPolicy: () => "allow",
}));

vi.mock("../terminal/ansi.js", () => ({
  sanitizeForLog: (s: string) => s,
}));

vi.mock("../trajectory/runtime.js", () => ({
  createTrajectoryRuntimeRecorder: () => ({
    enabled: true,
    filePath: "/tmp/session.trajectory.jsonl",
    recordEvent: (...args: unknown[]) => state.trajectoryRecordEventMock(...args),
    flush: () => state.trajectoryFlushMock(),
  }),
}));

vi.mock("../utils/message-channel.js", () => ({
  INTERNAL_MESSAGE_CHANNEL: "internal",
  isDeliverableMessageChannel: (value: string) => value !== "internal",
  normalizeMessageChannel: (value?: string | null) => value?.trim().toLowerCase() || undefined,
  resolveMessageChannel: (...values: Array<string | null | undefined>) =>
    values
      .find((value) => value?.trim())
      ?.trim()
      .toLowerCase(),
}));

vi.mock("./agent-scope.js", () => ({
  clearAutoFallbackPrimaryProbeSelection: vi.fn(),
  entryMatchesAutoFallbackPrimaryProbe: () => true,
  hasLegacyAutoFallbackWithoutOrigin: (entry: unknown) =>
    state.hasLegacyAutoFallbackWithoutOriginMock(entry),
  hasSessionAutoModelFallbackProvenance: () => false,
  listAgentEntries: () => [],
  listAgentIds: () => ["default"],
  markAutoFallbackPrimaryProbe: vi.fn(),
  resolveAutoFallbackPrimaryProbe: (params: unknown) =>
    state.resolveAutoFallbackPrimaryProbeMock(params),
  resolveAgentConfig: () => undefined,
  resolveAgentDir: () => "/tmp/agent",
  resolveDefaultAgentId: () => "default",
  resolveEffectiveModelFallbacks: state.resolveEffectiveModelFallbacksMock,
  resolveSessionAgentIds: () => ({ defaultAgentId: "default", sessionAgentId: "default" }),
  resolveSessionAgentId: () => "default",
  resolveAgentSkillsFilter: () => undefined,
  resolveAgentWorkspaceDir: () => "/tmp/workspace",
}));

vi.mock("./auth-profiles.js", () => ({
  ensureAuthProfileStore: () => ({ profiles: {} }),
}));

vi.mock("./auth-profiles/store.js", () => ({
  ensureAuthProfileStore: () => state.authProfileStoreMock,
}));

vi.mock("./auth-profiles/session-override.js", () => ({
  clearSessionAuthProfileOverride: (...args: unknown[]) =>
    state.clearSessionAuthProfileOverrideMock(...args),
}));

vi.mock("./defaults.js", () => ({
  DEFAULT_MODEL: "claude",
  DEFAULT_PROVIDER: "anthropic",
}));

vi.mock("./lanes.js", () => ({
  AGENT_LANE_SUBAGENT: "subagent",
}));

vi.mock("./model-catalog.js", () => ({
  loadManifestModelCatalog: state.loadManifestModelCatalogMock,
}));

vi.mock("./model-selection.js", () => {
  const normalizeProviderId = (provider: string) => provider.trim().toLowerCase();
  const buildAllowedModelSet = ({
    cfg,
    catalog,
    defaultProvider,
    defaultModel,
  }: {
    cfg?: unknown;
    catalog?: Array<{ provider: string; id: string }>;
    defaultProvider: string;
    defaultModel?: string;
  }) => {
    const modelMap =
      (cfg as { agents?: { defaults?: { models?: Record<string, unknown> } } } | undefined)?.agents
        ?.defaults?.models ?? {};
    const configuredCatalog = (
      (cfg as { models?: { providers?: Record<string, { models?: unknown[] }> } } | undefined)
        ?.models?.providers
        ? Object.entries(
            (cfg as { models?: { providers?: Record<string, { models?: unknown[] }> } }).models!
              .providers!,
          ).flatMap(([provider, entry]) =>
            Array.isArray(entry?.models)
              ? entry.models
                  .filter(
                    (model): model is Record<string, unknown> =>
                      Boolean(model) && typeof model === "object",
                  )
                  .map((model) => {
                    const id = typeof model.id === "string" ? model.id : "";
                    return {
                      provider,
                      id,
                      name: typeof model.name === "string" ? model.name : id,
                      reasoning: typeof model.reasoning === "boolean" ? model.reasoning : undefined,
                      compat: model.compat,
                    };
                  })
                  .filter((model) => model.id)
              : [],
          )
        : []
    ) as Array<{ provider: string; id: string }>;
    const combinedCatalog = [...(catalog ?? []), ...configuredCatalog];
    const allowedKeys = new Set<string>(
      Object.keys(modelMap).map((ref) => {
        const [provider, ...modelParts] = ref.split("/");
        return `${provider}/${modelParts.join("/")}`;
      }),
    );
    if (defaultModel) {
      allowedKeys.add(`${defaultProvider}/${defaultModel}`);
    }
    if (Object.keys(modelMap).length === 0) {
      return {
        allowedKeys,
        allowedCatalog: combinedCatalog,
        allowAny: true,
      };
    }
    return {
      allowedKeys,
      allowedCatalog: combinedCatalog.filter((entry) =>
        allowedKeys.has(`${entry.provider}/${entry.id}`),
      ),
      allowAny: false,
    };
  };
  return {
    buildAllowedModelSet,
    createModelVisibilityPolicy: (params: {
      cfg?: unknown;
      catalog?: Array<{ provider: string; id: string }>;
      defaultProvider: string;
      defaultModel?: string;
    }) => {
      const allowed = buildAllowedModelSet(params);
      const allowsKey = (key: string) => {
        if (allowed.allowAny || allowed.allowedKeys.has(key)) {
          return true;
        }
        const slash = key.indexOf("/");
        return slash > 0 && allowed.allowedKeys.has(`${key.slice(0, slash)}/*`);
      };
      return {
        ...allowed,
        exactModelRefs: [],
        providerWildcards: new Set<string>(),
        hasConfiguredEntries: !allowed.allowAny,
        hasProviderWildcards: [...allowed.allowedKeys].some((key) => key.endsWith("/*")),
        allowsKey,
        allows: ({ provider, model }: { provider: string; model: string }) =>
          allowsKey(`${provider}/${model}`),
        resolveSelection: ({ provider, model }: { provider: string; model: string }) => {
          const key = `${provider}/${model}`;
          if (allowsKey(key)) {
            return { provider, model };
          }
          const fallback = allowed.allowedCatalog[0];
          return fallback ? { provider: fallback.provider, model: fallback.id } : null;
        },
        visibleCatalog: ({ catalog }: { catalog: Array<{ provider: string; id: string }> }) =>
          catalog,
      };
    },
    buildConfiguredModelCatalog: ({ cfg }: { cfg?: unknown }) => {
      const providers = (cfg as { models?: { providers?: Record<string, { models?: unknown[] }> } })
        ?.models?.providers;
      if (!providers) {
        return [];
      }
      return Object.entries(providers).flatMap(([provider, entry]) =>
        Array.isArray(entry?.models)
          ? entry.models
              .filter(
                (model): model is Record<string, unknown> =>
                  Boolean(model) && typeof model === "object",
              )
              .map((model) => {
                const id = typeof model.id === "string" ? model.id : "";
                return {
                  provider,
                  id,
                  name: typeof model.name === "string" ? model.name : id,
                  reasoning: typeof model.reasoning === "boolean" ? model.reasoning : undefined,
                  compat: model.compat,
                };
              })
              .filter((model) => model.id)
          : [],
      );
    },
    isModelKeyAllowedBySet: (allowedKeys: ReadonlySet<string>, key: string) => {
      if (allowedKeys.has(key)) {
        return true;
      }
      const slash = key.indexOf("/");
      return slash > 0 && allowedKeys.has(`${key.slice(0, slash)}/*`);
    },
    resolveAllowedModelSelection: ({
      provider,
      model,
      allowAny,
      allowedKeys,
      allowedCatalog,
    }: {
      provider: string;
      model: string;
      allowAny: boolean;
      allowedKeys: ReadonlySet<string>;
      allowedCatalog: Array<{ provider: string; id: string }>;
    }) => {
      const key = `${provider}/${model}`;
      if (
        allowAny ||
        allowedKeys.has(key) ||
        (key.includes("/") && allowedKeys.has(`${key.slice(0, key.indexOf("/"))}/*`))
      ) {
        return { provider, model };
      }
      const fallback = allowedCatalog[0];
      return fallback ? { provider: fallback.provider, model: fallback.id } : null;
    },
    buildModelAliasIndex: ({
      cfg,
    }: {
      cfg?: { agents?: { defaults?: { models?: Record<string, { alias?: string }> } } };
    }) => {
      const byAlias = new Map<
        string,
        { alias: string; ref: { provider: string; model: string } }
      >();
      const byKey = new Map<string, string[]>();
      for (const [ref, entry] of Object.entries(cfg?.agents?.defaults?.models ?? {})) {
        const alias = entry?.alias?.trim();
        if (!alias) {
          continue;
        }
        const [provider, ...modelParts] = ref.split("/");
        const model = modelParts.join("/");
        byAlias.set(alias.toLowerCase(), { alias, ref: { provider, model } });
        byKey.set(`${provider}/${model}`, [alias]);
      }
      return { byAlias, byKey };
    },
    modelKey: (p: string, m: string) => `${p}/${m}`,
    normalizeModelRef: (p: string, m: string) => ({ provider: normalizeProviderId(p), model: m }),
    normalizeProviderId,
    normalizeProviderIdForAuth: normalizeProviderId,
    parseModelRef: (m: string, p: string) => {
      const slash = m.indexOf("/");
      return slash > 0
        ? { provider: m.slice(0, slash), model: m.slice(slash + 1) }
        : { provider: p, model: m };
    },
    resolveModelRefFromString: ({
      raw,
      defaultProvider,
      aliasIndex,
    }: {
      raw: string;
      defaultProvider: string;
      aliasIndex?: {
        byAlias: Map<string, { alias: string; ref: { provider: string; model: string } }>;
      };
    }) => {
      const aliasMatch = aliasIndex?.byAlias.get(raw.trim().toLowerCase());
      if (aliasMatch) {
        return { ref: aliasMatch.ref, alias: aliasMatch.alias };
      }
      const slash = raw.indexOf("/");
      return {
        ref:
          slash > 0
            ? { provider: raw.slice(0, slash), model: raw.slice(slash + 1) }
            : { provider: defaultProvider, model: raw },
      };
    },
    resolveConfiguredModelRef: ({ cfg }: { cfg?: unknown }) => {
      const raw = (cfg as { agents?: { defaults?: { model?: string | { primary?: string } } } })
        ?.agents?.defaults?.model;
      const primary = typeof raw === "string" ? raw : raw?.primary;
      const [provider, ...modelParts] = (primary ?? "anthropic/claude").split("/");
      return { provider, model: modelParts.join("/") || "claude" };
    },
    resolveDefaultModelForAgent: ({ cfg }: { cfg?: unknown }) => {
      const raw = (cfg as { agents?: { defaults?: { model?: string | { primary?: string } } } })
        ?.agents?.defaults?.model;
      const primary = typeof raw === "string" ? raw : raw?.primary;
      const [provider, ...modelParts] = (primary ?? "anthropic/claude").split("/");
      return { provider, model: modelParts.join("/") || "claude" };
    },
    resolveThinkingDefault: (args: unknown) => state.resolveThinkingDefaultMock(args),
  };
});

vi.mock("./model-visibility-policy.js", () => ({
  createModelVisibilityPolicy: ({
    cfg,
    catalog,
    defaultProvider,
    defaultModel,
  }: {
    cfg?: unknown;
    catalog?: Array<{ provider: string; id: string }>;
    defaultProvider: string;
    defaultModel?: string;
  }) => {
    const modelMap =
      (cfg as { agents?: { defaults?: { models?: Record<string, unknown> } } } | undefined)?.agents
        ?.defaults?.models ?? {};
    const allowedKeys = new Set<string>(
      Object.keys(modelMap).map((ref) => {
        const [provider, ...modelParts] = ref.split("/");
        return `${provider}/${modelParts.join("/")}`;
      }),
    );
    if (defaultModel) {
      allowedKeys.add(`${defaultProvider}/${defaultModel}`);
    }
    const allowAny = Object.keys(modelMap).length === 0;
    const allowedCatalog = allowAny
      ? (catalog ?? [])
      : (catalog ?? []).filter((entry) => allowedKeys.has(`${entry.provider}/${entry.id}`));
    const allowsKey = (key: string) => {
      if (allowAny || allowedKeys.has(key)) {
        return true;
      }
      const slash = key.indexOf("/");
      return slash > 0 && allowedKeys.has(`${key.slice(0, slash)}/*`);
    };
    return {
      allowAny,
      allowedKeys,
      allowedCatalog,
      exactModelRefs: [],
      providerWildcards: new Set<string>(),
      hasConfiguredEntries: !allowAny,
      hasProviderWildcards: [...allowedKeys].some((key) => key.endsWith("/*")),
      allowsKey,
      allows: ({ provider, model }: { provider: string; model: string }) =>
        allowsKey(`${provider}/${model}`),
      resolveSelection: ({ provider, model }: { provider: string; model: string }) => {
        const key = `${provider}/${model}`;
        if (allowsKey(key)) {
          return { provider, model };
        }
        const fallback = allowedCatalog[0];
        return fallback ? { provider: fallback.provider, model: fallback.id } : null;
      },
      visibleCatalog: ({
        catalog: catalogLocal,
      }: {
        catalog: Array<{ provider: string; id: string }>;
      }) => catalogLocal,
    };
  },
}));

vi.mock("./provider-auth-aliases.js", () => ({
  resolveProviderAuthAliasMap: () => ({}),
  resolveProviderIdForAuth: (provider: string) =>
    provider.trim().toLowerCase() === "codex-cli" ? "openai" : provider.trim().toLowerCase(),
}));

vi.mock("../skills/discovery/agent-filter.js", () => ({
  resolveEffectiveAgentSkillFilter: (_cfg: unknown, agentId: string) =>
    state.resolveAgentSkillsFilterMock(_cfg, agentId),
}));

vi.mock("../skills/runtime/remote.js", () => ({
  getRemoteSkillEligibility: () => ({ eligible: false }),
}));

vi.mock("../skills/runtime/session-snapshot.js", () => ({
  resolveReusableWorkspaceSkillSnapshot: (params: {
    workspaceDir: string;
    existingSnapshot?: { resolvedSkills?: unknown };
    skillFilter?: string[];
  }) => {
    if (params.skillFilter !== undefined && params.skillFilter.length === 0) {
      return {
        snapshot: {
          prompt: "",
          skills: [],
          resolvedSkills: [],
          skillFilter: params.skillFilter,
          version: 0,
        },
        shouldRefresh: !params.existingSnapshot,
        snapshotVersion: 0,
      };
    }
    if (params.existingSnapshot?.resolvedSkills !== undefined) {
      return {
        snapshot: params.existingSnapshot,
        shouldRefresh: false,
        snapshotVersion: 0,
      };
    }
    const rebuilt = state.buildWorkspaceSkillSnapshotMock(params.workspaceDir, params) as {
      resolvedSkills?: unknown;
    };
    return {
      snapshot: params.existingSnapshot
        ? { ...params.existingSnapshot, resolvedSkills: rebuilt?.resolvedSkills }
        : rebuilt,
      shouldRefresh: !params.existingSnapshot,
      snapshotVersion: 0,
    };
  },
}));

vi.mock("./spawned-context.js", () => ({
  normalizeSpawnedRunMetadata: (meta: unknown) => meta ?? {},
}));

vi.mock("./timeout.js", () => ({
  resolveAgentTimeoutMs: ({ overrideSeconds }: { overrideSeconds?: number | null }) =>
    typeof overrideSeconds === "number" && Number.isFinite(overrideSeconds)
      ? overrideSeconds === 0
        ? 2_147_483_647
        : Math.max(overrideSeconds * 1000, 1)
      : 30_000,
}));

vi.mock("./workspace.js", () => ({
  ensureAgentWorkspace: async () => ({ dir: "/tmp/workspace" }),
}));

vi.mock("../acp/control-plane/manager.js", () => ({
  getAcpSessionManager: () => ({
    resolveSession: (...args: unknown[]) => state.acpResolveSessionMock(...args),
    runTurn: (...args: unknown[]) => state.acpRunTurnMock(...args),
  }),
}));

let agentCommand: typeof import("./agent-command.js").agentCommand;
let agentCommandTesting: typeof import("./agent-command.js").testing;

beforeAll(async () => {
  const mod = await import("./agent-command.js");
  agentCommand ??= mod.agentCommand;
  agentCommandTesting ??= mod.testing;
});

type FallbackRunnerParams = {
  provider: string;
  model: string;
  sessionId?: string;
  run: (provider: string, model: string) => Promise<unknown>;
  onFallbackStep?: (step: Record<string, unknown>) => void | Promise<void>;
  classifyResult?: (params: {
    provider: string;
    model: string;
    result: unknown;
    attempt: number;
    total: number;
  }) => unknown;
};

type ModelSwitchOptions = ConstructorParameters<typeof LiveSessionModelSwitchError>[0];

function makeSuccessResult(provider: string, model: string) {
  return {
    payloads: [{ text: "ok" }],
    meta: {
      durationMs: 100,
      aborted: false,
      stopReason: "end_turn",
      agentMeta: { provider, model },
    },
  };
}

function makeEmptyResult(provider: string, model: string) {
  return {
    payloads: [],
    meta: {
      durationMs: 30_000,
      aborted: false,
      stopReason: "end_turn",
      agentHarnessResultClassification: "empty",
      agentMeta: { provider, model },
    },
  };
}

function setupModelSwitchRetry(switchOptions: ModelSwitchOptions) {
  let invocation = 0;
  state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
    invocation += 1;
    if (invocation === 1) {
      throw new LiveSessionModelSwitchError(switchOptions);
    }
    const result = await params.run(params.provider, params.model);
    return {
      result,
      provider: params.provider,
      model: params.model,
      attempts: [],
    };
  });
}

function setupSingleAttemptFallback() {
  state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
    const result = await params.run(params.provider, params.model);
    return {
      result,
      provider: params.provider,
      model: params.model,
      attempts: [],
    };
  });
}

function requireRecord(value: unknown, label: string): Record<string, unknown> {
  if (!value || typeof value !== "object") {
    throw new Error(`expected ${label} to be an object`);
  }
  return value as Record<string, unknown>;
}

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

function mockCallArg(mock: ReturnType<typeof vi.fn>, callIndex = 0, argIndex = 0): unknown {
  const call = mock.mock.calls[callIndex] as unknown[] | undefined;
  if (!call) {
    throw new Error(`expected mock call ${callIndex}`);
  }
  return call[argIndex];
}

function expectRecordFields(value: unknown, expected: Record<string, unknown>): void {
  const actual = requireRecord(value, "record");
  for (const [key, expectedValue] of Object.entries(expected)) {
    expect(actual[key]).toEqual(expectedValue);
  }
}

async function runBasicAgentCommand() {
  await agentCommand({
    message: "hello",
    to: "+1234567890",
  });
}

function setupSessionTouchStore(): void {
  const sessionEntry: SessionEntry = {
    sessionId: "session-1",
    updatedAt: 1,
    skillsSnapshot: { prompt: "", skills: [], version: 0 },
  };
  state.sessionEntryMock = sessionEntry;
  state.sessionStoreMock = { "agent:main:main": sessionEntry };
  state.storePathMock = "/tmp/openclaw-sessions.json";
}

function expectFallbackOverrideCalls(first: boolean, second: boolean) {
  expect(state.resolveEffectiveModelFallbacksMock).toHaveBeenCalledTimes(2);
  expectRecordFields(mockCallArg(state.resolveEffectiveModelFallbacksMock, 0), {
    hasSessionModelOverride: first,
  });
  expectRecordFields(mockCallArg(state.resolveEffectiveModelFallbacksMock, 1), {
    hasSessionModelOverride: second,
  });
}

describe("agentCommand – LiveSessionModelSwitchError retry", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    state.acpResolveSessionMock.mockReturnValue(null);
    state.resolveAcpAgentPolicyErrorMock.mockReturnValue(null);
    state.resolveAcpDispatchPolicyErrorMock.mockReturnValue(null);
    state.resolveAcpExplicitTurnPolicyErrorMock.mockReturnValue(null);
    state.runtimeConfigMock = undefined;
    delete (state.defaultRuntimeConfig.agents as { list?: unknown }).list;
    state.isThinkingLevelSupportedMock.mockReturnValue(true);
    state.resolveSupportedThinkingLevelMock.mockImplementation(
      ({ level }: { level?: string }) => level,
    );
    state.resolveThinkingDefaultMock.mockReturnValue("low");
    state.resolveAgentSkillsFilterMock.mockReturnValue(undefined);
    state.loadManifestModelCatalogMock.mockReturnValue([]);
    state.hasLegacyAutoFallbackWithoutOriginMock.mockReturnValue(false);
    state.applyModelOverrideToSessionEntryMock.mockReturnValue({ updated: false });
    state.resolveAutoFallbackPrimaryProbeMock.mockReturnValue(undefined);
    state.resolveChannelModelOverrideMock.mockImplementation((params: unknown) => {
      const input = params as {
        cfg?: { channels?: { modelByChannel?: Record<string, Record<string, string>> } };
        channel?: string;
        groupId?: string;
        parentSessionKey?: string;
      };
      const channel = input.channel?.trim().toLowerCase();
      const entries = channel ? input.cfg?.channels?.modelByChannel?.[channel] : undefined;
      if (!entries) {
        return null;
      }
      const direct = input.groupId ? entries[input.groupId] : undefined;
      if (direct) {
        return { channel, model: direct, matchKey: input.groupId };
      }
      const parentChannel = input.parentSessionKey?.match(/:channel:([^:]+)/u)?.[1];
      const parent = parentChannel ? entries[parentChannel] : undefined;
      return parent ? { channel, model: parent, matchKey: parentChannel } : null;
    });
    state.acpRunTurnMock.mockImplementation(async (params: unknown) => {
      const onEvent = (params as { onEvent?: (event: unknown) => void }).onEvent;
      onEvent?.({ type: "text_delta", stream: "output", text: "done" });
      onEvent?.({ type: "done", stopReason: "end_turn" });
    });
    state.createAcpVisibleTextAccumulatorMock.mockImplementation(() => {
      let text = "";
      return {
        consume(chunk: string) {
          text += chunk;
          return { text, delta: chunk };
        },
        finalizeRaw: () => text,
        finalize: () => text,
      };
    });
    state.buildAcpResultMock.mockImplementation((params: { payloadText?: string }) => ({
      payloads: params.payloadText ? [{ text: params.payloadText }] : [],
      meta: { durationMs: 0, stopReason: "end_turn" },
    }));
    state.persistCliTurnTranscriptMock.mockImplementation(
      async (params: { sessionEntry?: unknown }) => ({
        kind: "persisted",
        sessionEntry: params.sessionEntry,
      }),
    );
    state.persistAcpTurnTranscriptMock.mockImplementation(
      async (params: { sessionEntry?: unknown }) => ({
        kind: "persisted",
        sessionEntry: params.sessionEntry,
      }),
    );
    state.runCliTurnCompactionLifecycleMock.mockImplementation(
      async (params: { sessionEntry?: unknown }) => params.sessionEntry,
    );
    state.authProfileStoreMock = { profiles: {} };
    state.sessionEntryMock = undefined;
    state.sessionStoreMock = undefined;
    state.storePathMock = undefined;
    state.resolvedSessionKeyMock = undefined;
    state.persistSessionEntryMock.mockImplementation(async (...args: unknown[]) => {
      const params = args[0] as {
        sessionStore?: Record<string, unknown>;
        sessionKey?: string;
        entry?: unknown;
        shouldPersist?: (entry: unknown) => boolean;
      };
      const current =
        params.sessionStore && params.sessionKey
          ? params.sessionStore[params.sessionKey]
          : undefined;
      if (params.shouldPersist && !params.shouldPersist(current)) {
        if (current === undefined && params.sessionStore && params.sessionKey) {
          delete params.sessionStore[params.sessionKey];
        }
        return current;
      }
      if (params.sessionStore && params.sessionKey && params.entry) {
        params.sessionStore[params.sessionKey] = params.entry;
        return params.entry;
      }
      return current;
    });
    state.buildWorkspaceSkillSnapshotMock.mockReturnValue({
      prompt: "",
      skills: [],
      resolvedSkills: [],
      version: 0,
    });
    state.deliverAgentCommandResultMock.mockResolvedValue(undefined);
    state.resolveAgentOutboundTargetMock.mockImplementation(
      (params: { plan?: { resolvedTo?: string }; targetMode?: string }) => ({
        resolvedTarget: null,
        resolvedTo: params.plan?.resolvedTo,
        targetMode: params.targetMode ?? "implicit",
      }),
    );
    state.resolveMessageChannelSelectionMock.mockRejectedValue(new Error("channel required"));
    state.resolveAgentDeliveryPlanMock.mockImplementation(
      (params: {
        accountId?: string;
        explicitThreadId?: string | number;
        explicitTo?: string;
        requestedChannel?: string;
        sessionEntry?: SessionEntry;
      }) => {
        const context = params.sessionEntry?.deliveryContext;
        const channel =
          params.requestedChannel ??
          context?.channel ??
          params.sessionEntry?.lastChannel ??
          "internal";
        const to = params.explicitTo ?? context?.to ?? params.sessionEntry?.lastTo;
        const accountId =
          params.accountId ?? context?.accountId ?? params.sessionEntry?.lastAccountId;
        const threadId =
          params.explicitThreadId ?? context?.threadId ?? params.sessionEntry?.lastThreadId;
        return {
          baseDelivery: {},
          resolvedChannel: channel,
          resolvedTo: to,
          resolvedAccountId: accountId,
          resolvedThreadId: threadId,
          deliveryTargetMode: params.explicitTo ? "explicit" : to ? "implicit" : undefined,
        };
      },
    );
    state.updateSessionStoreAfterAgentRunMock.mockResolvedValue(undefined);
    state.trajectoryFlushMock.mockResolvedValue(undefined);
    state.prepareInternalSessionEffectsTranscriptMock.mockResolvedValue(
      "/tmp/openclaw-internal-run.jsonl",
    );
    state.removeInternalSessionEffectsTranscriptMock.mockResolvedValue(undefined);
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("retries with the switched provider/model when LiveSessionModelSwitchError is thrown", async () => {
    setupModelSwitchRetry({
      provider: "openai",
      model: "gpt-5.4",
    });

    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

    await runBasicAgentCommand();

    expect(state.runWithModelFallbackMock).toHaveBeenCalledTimes(2);

    const secondCall = mockCallArg(state.runWithModelFallbackMock, 1) as FallbackRunnerParams;
    expect(secondCall.provider).toBe("openai");
    expect(secondCall.model).toBe("gpt-5.4");
    expect(secondCall.sessionId).toBe("session-1");

    const lifecycleEndCalls = state.emitAgentEventMock.mock.calls.filter((call: unknown[]) => {
      const arg = call[0] as { stream?: string; data?: { phase?: string } };
      return arg?.stream === "lifecycle" && arg?.data?.phase === "end";
    });
    expect(lifecycleEndCalls.length).toBeGreaterThanOrEqual(1);
    const lifecycleFinishingCalls = state.emitAgentEventMock.mock.calls.filter(
      (call: unknown[]) => {
        const arg = call[0] as { stream?: string; data?: { phase?: string } };
        return arg?.stream === "lifecycle" && arg?.data?.phase === "finishing";
      },
    );
    expect(lifecycleFinishingCalls.length).toBeGreaterThanOrEqual(1);
    expectRecordFields(mockCallArg(state.runAgentAttemptMock), {
      deferTerminalLifecycle: true,
    });
    const firstFinishingIndex = state.emitAgentEventMock.mock.calls.findIndex((call: unknown[]) => {
      const arg = call[0] as { stream?: string; data?: { phase?: string } };
      return arg?.stream === "lifecycle" && arg?.data?.phase === "finishing";
    });
    const lastEndIndex = state.emitAgentEventMock.mock.calls.findLastIndex((call: unknown[]) => {
      const arg = call[0] as { stream?: string; data?: { phase?: string } };
      return arg?.stream === "lifecycle" && arg?.data?.phase === "end";
    });
    expect(state.deliverAgentCommandResultMock).toHaveBeenCalledTimes(1);
    const deliveryOrder = state.deliverAgentCommandResultMock.mock.invocationCallOrder[0] ?? 0;
    expect(
      state.emitAgentEventMock.mock.invocationCallOrder[firstFinishingIndex] ?? 0,
    ).toBeLessThan(deliveryOrder);
    expect(deliveryOrder).toBeLessThan(
      state.emitAgentEventMock.mock.invocationCallOrder[lastEndIndex] ?? 0,
    );
  });

  it("keeps the fast mode cutoff timestamp across live model switch retries", async () => {
    let invocation = 0;
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      invocation++;
      const result = await params.run(params.provider, params.model);
      if (invocation === 1) {
        throw new LiveSessionModelSwitchError({
          provider: "openai",
          model: "gpt-5.4",
        });
      }
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [],
      };
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

    await runBasicAgentCommand();

    const firstAttempt = mockCallArg(state.runAgentAttemptMock, 0) as {
      fastModeStartedAtMs?: number;
    };
    const secondAttempt = mockCallArg(state.runAgentAttemptMock, 1) as {
      fastModeStartedAtMs?: number;
    };
    expect(firstAttempt.fastModeStartedAtMs).toBe(secondAttempt.fastModeStartedAtMs);
  });

  it("reuses durable user-turn proof across live model switch retries", async () => {
    let fallbackInvocation = 0;
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      fallbackInvocation += 1;
      const result = await params.run(params.provider, params.model);
      if (fallbackInvocation === 1) {
        throw new LiveSessionModelSwitchError({
          provider: "openai",
          model: "gpt-5.4",
        });
      }
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [],
      };
    });
    state.runAgentAttemptMock.mockImplementation(async (attemptParams: unknown) => {
      const attempt = attemptParams as {
        userTurnTranscriptRecorder?: {
          markRuntimePersisted: (message: { role: "user"; content: string }) => void;
        };
      };
      if (state.runAgentAttemptMock.mock.calls.length === 1) {
        attempt.userTurnTranscriptRecorder?.markRuntimePersisted({
          role: "user",
          content: "hello",
        });
      }
      return makeSuccessResult("openai", "gpt-5.4");
    });

    await runBasicAgentCommand();

    const firstAttempt = mockCallArg(state.runAgentAttemptMock, 0) as {
      suppressPromptPersistenceOnRetry?: boolean;
      userTurnTranscriptRecorder?: unknown;
    };
    const secondAttempt = mockCallArg(state.runAgentAttemptMock, 1) as {
      suppressPromptPersistenceOnRetry?: boolean;
      userTurnTranscriptRecorder?: unknown;
    };
    expect(secondAttempt.userTurnTranscriptRecorder).toBe(firstAttempt.userTurnTranscriptRecorder);
    expect(firstAttempt.suppressPromptPersistenceOnRetry).toBe(false);
    expect(secondAttempt.suppressPromptPersistenceOnRetry).toBe(true);
  });

  it("uses an embedded queue rebound generation for terminal lifecycle and cleanup", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockImplementation(async (attemptParams: unknown) => {
      (
        attemptParams as {
          onLifecycleGenerationChanged?: (lifecycleGeneration: string) => void;
        }
      ).onLifecycleGenerationChanged?.("post-restart-generation");
      return makeSuccessResult("openai", "gpt-5.4");
    });

    await runBasicAgentCommand();

    const lifecycleEnd = state.emitAgentEventMock.mock.calls
      .map(
        (call) =>
          call[0] as {
            stream?: string;
            data?: { phase?: string };
            lifecycleGeneration?: string;
          },
      )
      .find((event) => event.stream === "lifecycle" && event.data?.phase === "end");
    expect(lifecycleEnd?.lifecycleGeneration).toBe("post-restart-generation");
    expect(state.clearAgentRunContextMock).toHaveBeenCalledWith(
      expect.any(String),
      "post-restart-generation",
    );
  });

  it("preserves restart ownership when an aborted attempt resolves normally", async () => {
    setupSingleAttemptFallback();
    const controller = new AbortController();
    state.runAgentAttemptMock.mockImplementation(async () => {
      controller.abort(createAgentRunRestartAbortError());
      return {
        payloads: [],
        meta: {
          durationMs: 100,
          aborted: true,
          stopReason: "end_turn",
          agentMeta: { provider: "anthropic", model: "claude" },
        },
      };
    });

    await expect(
      agentCommand({
        message: "hello",
        to: "+1234567890",
        abortSignal: controller.signal,
      }),
    ).rejects.toThrow("agent run aborted for restart");

    const lifecycleEvents = state.emitAgentEventMock.mock.calls
      .map((call) => call[0] as { stream?: string; data?: Record<string, unknown> })
      .filter((event) => event.stream === "lifecycle");
    expect(lifecycleEvents).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          data: expect.objectContaining({
            phase: "error",
            aborted: true,
            stopReason: "restart",
          }),
        }),
      ]),
    );
    expect(state.deliverAgentCommandResultMock).not.toHaveBeenCalled();
  });

  it("preserves restart ownership when an aborted ACP turn resolves normally", async () => {
    state.acpResolveSessionMock.mockReturnValue({
      kind: "ready",
      meta: {
        agent: "claude",
        cwd: "/tmp/workspace",
      },
    });
    const controller = new AbortController();
    controller.abort(createAgentRunRestartAbortError());

    await expect(
      agentCommand({
        message: "hello",
        sessionKey: "agent:main:main",
        abortSignal: controller.signal,
      }),
    ).rejects.toThrow("agent run aborted for restart");

    expect(state.emitAcpLifecycleEndMock).not.toHaveBeenCalled();
    expect(state.deliverAgentCommandResultMock).not.toHaveBeenCalled();
  });

  it("suppresses ACP delivery when restart begins during transcript persistence", async () => {
    state.acpResolveSessionMock.mockReturnValue({
      kind: "ready",
      meta: {
        agent: "claude",
        cwd: "/tmp/workspace",
      },
    });
    const controller = new AbortController();
    state.persistAcpTurnTranscriptMock.mockImplementation(
      async (params: { sessionEntry?: unknown }) => {
        controller.abort(createAgentRunRestartAbortError());
        return { kind: "persisted", sessionEntry: params.sessionEntry };
      },
    );

    await expect(
      agentCommand({
        message: "hello",
        sessionKey: "agent:main:main",
        abortSignal: controller.signal,
      }),
    ).rejects.toThrow("agent run aborted for restart");

    expect(state.emitAcpLifecycleEndMock).not.toHaveBeenCalled();
    expect(state.emitAcpLifecycleErrorMock).toHaveBeenCalledWith(
      expect.objectContaining({
        runId: "session-1",
        sessionKey: "agent:main:main",
      }),
    );
    const lifecycleError = state.emitAcpLifecycleErrorMock.mock.calls[0]?.[0] as
      | { abortSignal?: AbortSignal }
      | undefined;
    expect(lifecycleError?.abortSignal?.aborted).toBe(true);
    expect(lifecycleError?.abortSignal?.reason).toBe(controller.signal.reason);
    expect(state.persistAcpTurnTranscriptMock).toHaveBeenCalledTimes(1);
    expect(state.buildAcpResultMock).not.toHaveBeenCalled();
    expect(state.deliverAgentCommandResultMock).not.toHaveBeenCalled();
  });

  it("threads lifecycle ownership into ACP delivery", async () => {
    state.acpResolveSessionMock.mockReturnValue({
      kind: "ready",
      meta: {
        agent: "claude",
        cwd: "/tmp/workspace",
      },
    });

    await agentCommand({
      message: "hello",
      sessionKey: "agent:main:main",
    });

    const deliveryParams = requireRecord(
      mockCallArg(state.deliverAgentCommandResultMock),
      "ACP delivery params",
    );
    expect(deliveryParams.assertDeliveryCurrent).toBeTypeOf("function");
    (deliveryParams.assertDeliveryCurrent as () => void)();
    expect(state.assertLifecycleCurrentMock).toHaveBeenLastCalledWith("test-generation");
  });

  it("keeps the initial session touch for local runs", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    setupSessionTouchStore();

    await runBasicAgentCommand();

    const touchWrites = state.persistSessionEntryMock.mock.calls.filter((call) => {
      const entry = (call[0] as { entry?: Record<string, unknown> } | undefined)?.entry;
      return entry?.lastInteractionAt !== undefined;
    });
    expect(touchWrites).toHaveLength(1);
    expect(state.updateSessionStoreAfterAgentRunMock).toHaveBeenCalledTimes(1);
  });

  it("threads lifecycle ownership into normal delivery", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    setupSessionTouchStore();

    await runBasicAgentCommand();

    const deliveryParams = requireRecord(
      mockCallArg(state.deliverAgentCommandResultMock),
      "delivery params",
    );
    expect(deliveryParams.assertDeliveryCurrent).toBeTypeOf("function");
    (deliveryParams.assertDeliveryCurrent as () => void)();
    expect(state.assertLifecycleCurrentMock).toHaveBeenLastCalledWith("test-generation");
  });

  it("passes explicit timeout overrides into agent attempts", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

    await agentCommand({
      message: "hello",
      to: "+1234567890",
      timeout: "600",
    });

    expectRecordFields(mockCallArg(state.runAgentAttemptMock), {
      timeoutMs: 600_000,
      runTimeoutOverrideMs: 600_000,
    });
  });

  it("clamps unsupported explicit thinking for subagent spawns instead of throwing", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude-fable-5"));
    state.resolvedSessionKeyMock = "agent:planner:subagent:00000000-0000-4000-8000-000000000000";
    state.isThinkingLevelSupportedMock.mockReturnValue(false);
    state.resolveSupportedThinkingLevelMock.mockReturnValue("high");

    await agentCommand({
      message: "hello",
      sessionKey: state.resolvedSessionKeyMock,
      thinking: "xhigh",
      lane: "subagent",
    });

    expect(state.resolveSupportedThinkingLevelMock).toHaveBeenCalled();
    expectRecordFields(mockCallArg(state.runAgentAttemptMock), {
      resolvedThinkLevel: "high",
    });
  });

  it("rejects unsupported explicit thinking for interactive subagent-key runs", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude-fable-5"));
    state.resolvedSessionKeyMock = "agent:planner:subagent:00000000-0000-4000-8000-000000000000";
    state.isThinkingLevelSupportedMock.mockReturnValue(false);

    await expect(
      agentCommand({
        message: "hello",
        sessionKey: state.resolvedSessionKeyMock,
        thinking: "xhigh",
      }),
    ).rejects.toThrow(/is not supported/u);
    expect(state.runAgentAttemptMock).not.toHaveBeenCalled();
  });

  it("rejects unsupported explicit thinking for non-subagent sessions on the subagent lane", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude-fable-5"));
    state.resolvedSessionKeyMock = "agent:main:main";
    state.isThinkingLevelSupportedMock.mockReturnValue(false);

    await expect(
      agentCommand({
        message: "hello",
        sessionKey: state.resolvedSessionKeyMock,
        thinking: "xhigh",
        lane: "subagent",
      }),
    ).rejects.toThrow(/is not supported/u);
    expect(state.runAgentAttemptMock).not.toHaveBeenCalled();
  });

  it("rejects unsupported explicit thinking for direct interactive runs", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude-fable-5"));
    state.resolvedSessionKeyMock = "agent:main:main";
    state.isThinkingLevelSupportedMock.mockReturnValue(false);

    await expect(
      agentCommand({
        message: "hello",
        to: "+1234567890",
        thinking: "xhigh",
      }),
    ).rejects.toThrow(/is not supported/u);
    expect(state.runAgentAttemptMock).not.toHaveBeenCalled();
  });

  it("skips the initial session touch after gateway ingress already persisted activity", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    setupSessionTouchStore();

    await agentCommand({
      message: "hello",
      to: "+1234567890",
      skipInitialSessionTouch: true,
    });

    const touchWrites = state.persistSessionEntryMock.mock.calls.filter((call) => {
      const entry = (call[0] as { entry?: Record<string, unknown> } | undefined)?.entry;
      return entry?.lastInteractionAt !== undefined;
    });
    expect(touchWrites).toHaveLength(0);
    expect(state.updateSessionStoreAfterAgentRunMock).toHaveBeenCalledTimes(1);
  });

  it("uses channel model override as the initial run model for channel-backed sessions", async () => {
    setupSingleAttemptFallback();
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: "anthropic/default-model",
          models: {
            "anthropic/default-model": {},
            "openai/channel-model": {},
          },
        },
      },
      channels: {
        modelByChannel: {
          discord: {
            "channel-123": "openai/channel-model",
          },
        },
      },
    };
    state.sessionEntryMock = {
      sessionId: "session-1",
      updatedAt: 1,
      channel: "discord",
      groupId: "channel-123",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "channel-model"));

    await runBasicAgentCommand();

    expect(mockCallArg(state.resolveChannelModelOverrideMock)).toMatchObject({
      channel: "discord",
      groupId: "channel-123",
    });
    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("openai");
    expect(fallbackParams.model).toBe("channel-model");
    expectRecordFields(mockCallArg(state.runAgentAttemptMock), {
      providerOverride: "openai",
      modelOverride: "channel-model",
    });
  });

  it("uses current run channel context when persisted session metadata is absent", async () => {
    setupSingleAttemptFallback();
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: "anthropic/default-model",
          models: {
            "anthropic/default-model": {},
            "openai/channel-model": {},
          },
        },
      },
      channels: {
        modelByChannel: {
          discord: {
            "channel-123": "openai/channel-model",
          },
        },
      },
    };
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "channel-model"));

    await agentCommand({
      message: "hello",
      channel: "discord",
      groupId: "channel-123",
      to: "discord:channel:channel-123",
    });

    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("openai");
    expect(fallbackParams.model).toBe("channel-model");
  });

  it("keeps persisted channel model override when current run context is internal", async () => {
    setupSingleAttemptFallback();
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: "anthropic/default-model",
          models: {
            "anthropic/default-model": {},
            "openai/channel-model": {},
          },
        },
      },
      channels: {
        modelByChannel: {
          discord: {
            "channel-123": "openai/channel-model",
          },
        },
      },
    };
    state.sessionEntryMock = {
      sessionId: "session-1",
      updatedAt: 1,
      channel: "discord",
      groupId: "channel-123",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "channel-model"));

    await agentCommand({
      message: "hello",
      channel: "internal",
      messageChannel: "internal",
      to: "internal",
    });

    expect(mockCallArg(state.resolveChannelModelOverrideMock)).toMatchObject({
      channel: "discord",
      groupId: "channel-123",
    });
    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("openai");
    expect(fallbackParams.model).toBe("channel-model");
  });

  it("uses channel model override after ignoring stale legacy fallback overrides", async () => {
    setupSingleAttemptFallback();
    state.hasLegacyAutoFallbackWithoutOriginMock.mockReturnValue(true);
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: "anthropic/default-model",
          models: {
            "anthropic/default-model": {},
            "openai/channel-model": {},
          },
        },
      },
      channels: {
        modelByChannel: {
          discord: {
            "channel-123": "openai/channel-model",
          },
        },
      },
    };
    state.sessionEntryMock = {
      sessionId: "session-1",
      updatedAt: 1,
      channel: "discord",
      groupId: "channel-123",
      providerOverride: "anthropic",
      modelOverride: "stale-fallback-model",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "channel-model"));

    await runBasicAgentCommand();

    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("openai");
    expect(fallbackParams.model).toBe("channel-model");
  });

  it("uses a concurrent user override adopted during legacy fallback repair", async () => {
    setupSingleAttemptFallback();
    state.applyModelOverrideToSessionEntryMock.mockImplementation((params: unknown) => {
      const { entry } = params as { entry: SessionEntry };
      delete entry.providerOverride;
      delete entry.modelOverride;
      delete entry.modelOverrideSource;
      return { updated: true };
    });
    state.hasLegacyAutoFallbackWithoutOriginMock.mockImplementation(
      (entry: unknown) =>
        (entry as SessionEntry | undefined)?.modelOverride === "stale-fallback-model",
    );
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: "anthropic/default-model",
          models: {
            "anthropic/default-model": {},
            "anthropic/stale-fallback-model": {},
            "google/gemini-3-pro": {},
          },
        },
      },
    };
    const sessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
      providerOverride: "anthropic",
      modelOverride: "stale-fallback-model",
      modelOverrideSource: "auto",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    } satisfies SessionEntry;
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = { "agent:main:main": sessionEntry };
    state.storePathMock = "/tmp/openclaw-session-store.json";
    state.persistSessionEntryMock.mockImplementation(async (...args: unknown[]) => {
      const params = args[0] as { entry?: SessionEntry };
      if (params.entry?.modelOverride === "stale-fallback-model") {
        return params.entry;
      }
      return {
        ...sessionEntry,
        updatedAt: 2,
        providerOverride: "google",
        modelOverride: "gemini-3-pro",
        modelOverrideSource: "user",
      };
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("google", "gemini-3-pro"));

    await runBasicAgentCommand();

    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("google");
    expect(fallbackParams.model).toBe("gemini-3-pro");
  });

  it("probes the channel primary when a session is pinned to an auto fallback", async () => {
    setupSingleAttemptFallback();
    state.resolveAutoFallbackPrimaryProbeMock.mockReturnValue({
      provider: "openai",
      model: "channel-model",
      fallbackProvider: "anthropic",
      fallbackModel: "fallback-model",
    });
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: "anthropic/default-model",
          models: {
            "anthropic/default-model": {},
            "anthropic/fallback-model": {},
            "openai/channel-model": {},
          },
        },
      },
      channels: {
        modelByChannel: {
          discord: {
            "channel-123": "openai/channel-model",
          },
        },
      },
    };
    state.sessionEntryMock = {
      sessionId: "session-1",
      updatedAt: 1,
      channel: "discord",
      groupId: "channel-123",
      providerOverride: "anthropic",
      modelOverride: "fallback-model",
      modelOverrideSource: "auto",
      modelOverrideFallbackOriginProvider: "openai",
      modelOverrideFallbackOriginModel: "channel-model",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "channel-model"));

    await runBasicAgentCommand();

    expectRecordFields(mockCallArg(state.resolveAutoFallbackPrimaryProbeMock), {
      primaryProvider: "openai",
      primaryModel: "channel-model",
    });
    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("openai");
    expect(fallbackParams.model).toBe("channel-model");
  });

  it("uses current threaded session key for parent channel model overrides", async () => {
    setupSingleAttemptFallback();
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: "anthropic/default-model",
          models: {
            "anthropic/default-model": {},
            "openai/parent-channel-model": {},
          },
        },
      },
      channels: {
        modelByChannel: {
          slack: {
            general: "openai/parent-channel-model",
          },
        },
      },
    };
    state.resolvedSessionKeyMock = "agent:main:slack:channel:general:thread:thread-1";
    state.sessionEntryMock = {
      sessionId: "session-1",
      updatedAt: 1,
      channel: "slack",
      groupId: "thread-1",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    state.runAgentAttemptMock.mockResolvedValue(
      makeSuccessResult("openai", "parent-channel-model"),
    );

    await runBasicAgentCommand();

    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("openai");
    expect(fallbackParams.model).toBe("parent-channel-model");
  });

  it("keeps stored session model overrides ahead of channel model overrides", async () => {
    setupSingleAttemptFallback();
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: "anthropic/default-model",
          models: {
            "anthropic/default-model": {},
            "openai/channel-model": {},
            "anthropic/stored-model": {},
          },
        },
      },
      channels: {
        modelByChannel: {
          discord: {
            "channel-123": "openai/channel-model",
          },
        },
      },
    };
    state.sessionEntryMock = {
      sessionId: "session-1",
      updatedAt: 1,
      channel: "discord",
      groupId: "channel-123",
      providerOverride: "anthropic",
      modelOverride: "stored-model",
      modelOverrideSource: "user",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "stored-model"));

    await runBasicAgentCommand();

    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("anthropic");
    expect(fallbackParams.model).toBe("stored-model");
  });

  it("keeps explicit run model overrides ahead of channel model overrides", async () => {
    setupSingleAttemptFallback();
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: "anthropic/default-model",
          models: {
            "anthropic/default-model": {},
            "openai/channel-model": {},
            "openai/explicit-model": {},
          },
        },
      },
      channels: {
        modelByChannel: {
          discord: {
            "channel-123": "openai/channel-model",
          },
        },
      },
    };
    state.sessionEntryMock = {
      sessionId: "session-1",
      updatedAt: 1,
      channel: "discord",
      groupId: "channel-123",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "explicit-model"));

    await agentCommand({
      message: "hello",
      to: "+1234567890",
      model: "openai/explicit-model",
      allowModelOverride: true,
    });

    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("openai");
    expect(fallbackParams.model).toBe("explicit-model");
  });

  it("uses rotated session identity for all post-run session persistence", async () => {
    setupSingleAttemptFallback();
    setupSessionTouchStore();
    const rotatedEntry: SessionEntry = {
      sessionId: "rotated-session",
      sessionFile: "/tmp/rotated-session.jsonl",
      updatedAt: 2,
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    const result = makeSuccessResult("openai", "gpt-5.4") as ReturnType<
      typeof makeSuccessResult
    > & {
      meta: Record<string, unknown> & { agentMeta: Record<string, unknown> };
    };
    result.meta.executionTrace = {
      runner: "embedded",
      fallbackUsed: false,
      winnerProvider: "openai",
      winnerModel: "gpt-5.4",
    };
    result.meta.finalAssistantVisibleText = "ok";
    result.meta.agentMeta = {
      ...result.meta.agentMeta,
      sessionId: "rotated-session",
      sessionFile: "/tmp/rotated-session.jsonl",
    };
    state.runAgentAttemptMock.mockResolvedValue(result);
    state.updateSessionStoreAfterAgentRunMock.mockImplementation(async () => {
      state.sessionStoreMock = { "agent:main:main": rotatedEntry };
    });
    state.persistCliTurnTranscriptMock.mockResolvedValue({
      kind: "persisted",
      sessionEntry: rotatedEntry,
    });
    state.runCliTurnCompactionLifecycleMock.mockResolvedValue(rotatedEntry);

    await runBasicAgentCommand();

    expectRecordFields(mockCallArg(state.updateSessionStoreAfterAgentRunMock), {
      sessionId: "rotated-session",
    });
    expectRecordFields(mockCallArg(state.persistCliTurnTranscriptMock), {
      sessionId: "rotated-session",
      sessionKey: "agent:main:main",
    });
    expectRecordFields(mockCallArg(state.runCliTurnCompactionLifecycleMock), {
      sessionId: "rotated-session",
      sessionKey: "agent:main:main",
    });
    expectRecordFields(mockCallArg(state.deliverAgentCommandResultMock), {
      expectedSessionIdForFreshDelivery: "rotated-session",
    });
  });

  it("skips post-run persistence after the session is deleted", async () => {
    setupSingleAttemptFallback();
    setupSessionTouchStore();
    const result = makeSuccessResult("openai", "gpt-5.4") as ReturnType<
      typeof makeSuccessResult
    > & {
      meta: Record<string, unknown> & { executionTrace: Record<string, unknown> };
    };
    result.meta.executionTrace = {
      runner: "cli",
      fallbackUsed: false,
      winnerProvider: "openai",
      winnerModel: "gpt-5.4",
    };
    state.runAgentAttemptMock.mockResolvedValue(result);
    state.persistCliTurnTranscriptMock.mockResolvedValue({
      kind: "session-rebound",
      sessionEntry: undefined,
    });

    await runBasicAgentCommand();

    expect(state.persistCliTurnTranscriptMock).toHaveBeenCalledTimes(1);
    expect(state.runCliTurnCompactionLifecycleMock).not.toHaveBeenCalled();
    expect(state.deliverAgentCommandResultMock).toHaveBeenCalledTimes(1);
  });

  it("does not treat backend CLI session id as OpenClaw session identity", async () => {
    setupSingleAttemptFallback();
    setupSessionTouchStore();
    const result = makeSuccessResult("openai", "gpt-5.4") as ReturnType<
      typeof makeSuccessResult
    > & {
      meta: Record<string, unknown> & { agentMeta: Record<string, unknown> };
    };
    result.meta.agentMeta = {
      ...result.meta.agentMeta,
      sessionId: "backend-cli-session",
    };
    state.runAgentAttemptMock.mockResolvedValue(result);

    await runBasicAgentCommand();

    expectRecordFields(mockCallArg(state.updateSessionStoreAfterAgentRunMock), {
      sessionId: "session-1",
    });
    expectRecordFields(mockCallArg(state.deliverAgentCommandResultMock), {
      expectedSessionIdForFreshDelivery: "session-1",
    });
  });

  it("scopes explicit-agent sentinel store keys before command routing", () => {
    expect(
      agentCommandTesting.resolveExplicitAgentCommandSessionKey({
        rawExplicitSessionKey: "global",
        agentIdOverride: "work",
        cfg: {},
      }),
    ).toBe("agent:work:global");
    expect(
      agentCommandTesting.resolveExplicitAgentCommandSessionKey({
        rawExplicitSessionKey: "main",
        agentIdOverride: "work",
        cfg: {},
      }),
    ).toBe("agent:work:main");
  });

  it("persists explicit overrides even when ingress skips the initial touch", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    setupSessionTouchStore();

    await agentCommand({
      message: "hello",
      to: "+1234567890",
      thinking: "medium",
      skipInitialSessionTouch: true,
    });

    const touchWrite = state.persistSessionEntryMock.mock.calls.find((call) => {
      const entry = (call[0] as { entry?: Record<string, unknown> } | undefined)?.entry;
      return entry?.thinkingLevel === "medium";
    })?.[0] as { entry?: Record<string, unknown> } | undefined;
    expect(touchWrite?.entry?.lastInteractionAt).toBeDefined();
    expect(state.updateSessionStoreAfterAgentRunMock).toHaveBeenCalledTimes(1);
  });

  it("does not persist turn-local thinking fallback over a stored session override", async () => {
    setupSingleAttemptFallback();
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
      thinkingLevel: "high",
    };
    const sessionStore: Record<string, SessionEntry> = { "agent:main:main": sessionEntry };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.isThinkingLevelSupportedMock.mockReturnValue(false);
    state.resolveSupportedThinkingLevelMock.mockReturnValue("off");
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

    await runBasicAgentCommand();

    expectRecordFields(mockCallArg(state.runAgentAttemptMock), {
      resolvedThinkLevel: "off",
    });
    expect(sessionEntry.thinkingLevel).toBe("high");
    expect(sessionStore["agent:main:main"]?.thinkingLevel).toBe("high");
    expect(state.persistSessionEntryMock).not.toHaveBeenCalledWith(
      expect.objectContaining({
        entry: expect.objectContaining({ thinkingLevel: "off" }),
      }),
    );
  });

  it("persists and clears current run delivery context for restart recovery", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = { "agent:main:main": sessionEntry };
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockResolvedValue({ deliverySucceeded: true });

    await agentCommand({
      message: "hello",
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
      threadId: "reply-1",
      deliver: true,
    });

    const persistedContexts = state.persistSessionEntryMock.mock.calls.map((call) => {
      const params = call[0] as { entry?: SessionEntry };
      return params.entry?.restartRecoveryDeliveryContext;
    });
    expect(persistedContexts).toContainEqual({
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
      threadId: "reply-1",
    });
    const stored = (state.sessionStoreMock as Record<string, SessionEntry>)["agent:main:main"];
    expect(stored?.restartRecoveryDeliveryContext).toBeUndefined();
  });

  it("preserves parsed explicit target threads for restart recovery", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = { "agent:main:main": sessionEntry };
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockResolvedValue({ deliverySucceeded: true });
    state.resolveAgentDeliveryPlanMock.mockReturnValueOnce({
      baseDelivery: {
        mode: "explicit",
        threadId: "thread-1",
        threadIdSource: "explicit",
      },
      resolvedChannel: "discord",
      resolvedTo: "discord:channel:general",
      resolvedAccountId: "main",
      resolvedThreadId: "thread-1",
      deliveryTargetMode: "explicit",
    });

    await agentCommand({
      message: "hello",
      channel: "discord",
      to: "discord:channel:general/thread:thread-1",
      accountId: "main",
      deliver: true,
    });

    const persistedContexts = state.persistSessionEntryMock.mock.calls.map((call) => {
      const params = call[0] as { entry?: SessionEntry };
      return params.entry?.restartRecoveryDeliveryContext;
    });
    expect(persistedContexts).toContainEqual({
      channel: "discord",
      to: "discord:channel:general",
      accountId: "main",
      threadId: "thread-1",
    });
  });

  it("does not inherit a stale thread when restart recovery uses an explicit target", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
      lastThreadId: "stale-thread",
    };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = { "agent:main:main": sessionEntry };
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockResolvedValue({ deliverySucceeded: true });

    await agentCommand({
      message: "hello",
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
      deliver: true,
    });

    const persistedContexts = state.persistSessionEntryMock.mock.calls.map((call) => {
      const params = call[0] as { entry?: SessionEntry };
      return params.entry?.restartRecoveryDeliveryContext;
    });
    expect(persistedContexts).toContainEqual({
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
    });
  });

  it("persists implicit session delivery route for restart recovery", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
      deliveryContext: {
        channel: "discord",
        to: "discord:channel:general",
        accountId: "main",
        threadId: "thread-1",
      },
    };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = { "agent:main:main": sessionEntry };
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockResolvedValue({ deliverySucceeded: true });

    await agentCommand({
      message: "hello",
      sessionKey: "agent:main:main",
      deliver: true,
    });

    const persistedContexts = state.persistSessionEntryMock.mock.calls.map((call) => {
      const params = call[0] as { entry?: SessionEntry };
      return params.entry?.restartRecoveryDeliveryContext;
    });
    expect(persistedContexts).toContainEqual({
      channel: "discord",
      to: "discord:channel:general",
      accountId: "main",
      threadId: "thread-1",
    });
    expect(state.resolveAgentDeliveryPlanMock).toHaveBeenCalledWith(
      expect.objectContaining({
        explicitTo: undefined,
        requestedChannel: undefined,
        sessionEntry: expect.objectContaining({
          deliveryContext: expect.objectContaining({ to: "discord:channel:general" }),
        }),
        wantsDelivery: true,
      }),
    );
  });

  it("persists default target delivery route for restart recovery", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = { "agent:main:main": sessionEntry };
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockResolvedValue({ deliverySucceeded: true });
    state.resolveMessageChannelSelectionMock.mockResolvedValue({
      channel: "discord",
      configured: ["discord"],
      source: "single-configured",
    });
    state.resolveAgentOutboundTargetMock.mockReturnValue({
      resolvedTarget: { ok: true, to: "discord:channel:default" },
      resolvedTo: "discord:channel:default",
      targetMode: "implicit",
    });

    await agentCommand({
      message: "hello",
      sessionKey: "agent:main:main",
      deliver: true,
    });

    const persistedContexts = state.persistSessionEntryMock.mock.calls.map((call) => {
      const params = call[0] as { entry?: SessionEntry };
      return params.entry?.restartRecoveryDeliveryContext;
    });
    expect(persistedContexts).toContainEqual({
      channel: "discord",
      to: "discord:channel:default",
    });
  });

  it("does not overwrite another active run's restart recovery context", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeEmptyResult("openai", "gpt-5.4"));
    const staleEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    const laterRunEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 2,
      restartRecoveryDeliveryContext: {
        channel: "discord",
        to: "discord:dm:456",
        accountId: "main",
      },
      restartRecoveryDeliveryRunId: "later-run",
    };
    const sessionStore: Record<string, SessionEntry> = {
      "agent:main:main": laterRunEntry,
    };
    state.sessionEntryMock = staleEntry;
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-sessions.json";

    await agentCommand({
      message: "hello",
      sessionKey: "agent:main:main",
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
      deliver: true,
      runId: "stale-run",
    });

    expect(sessionStore["agent:main:main"]?.restartRecoveryDeliveryContext).toEqual(
      laterRunEntry.restartRecoveryDeliveryContext,
    );
    expect(sessionStore["agent:main:main"]?.restartRecoveryDeliveryRunId).toBe("later-run");
  });

  it("does not clear another active run's restart recovery context", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeEmptyResult("openai", "gpt-5.4"));
    const staleEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    const laterRunEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 2,
      restartRecoveryDeliveryContext: {
        channel: "discord",
        to: "discord:dm:456",
        accountId: "main",
      },
      restartRecoveryDeliveryRunId: "later-run",
    };
    const sessionStore: Record<string, SessionEntry> = {
      "agent:main:main": laterRunEntry,
    };
    state.sessionEntryMock = staleEntry;
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-sessions.json";

    await agentCommand({
      message: "hello",
      sessionKey: "agent:main:main",
      deliver: true,
      runId: "stale-run",
    });

    expect(sessionStore["agent:main:main"]?.restartRecoveryDeliveryContext).toEqual(
      laterRunEntry.restartRecoveryDeliveryContext,
    );
    expect(sessionStore["agent:main:main"]?.restartRecoveryDeliveryRunId).toBe("later-run");
  });

  it("keeps current run delivery context when restart marker wins the cleanup race", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    const sessionStore: Record<string, SessionEntry> = { "agent:main:main": sessionEntry };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockImplementation(async () => {
      const current = sessionStore["agent:main:main"];
      if (current) {
        current.abortedLastRun = true;
      }
      return { deliverySucceeded: false };
    });

    await agentCommand({
      message: "hello",
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
      deliver: true,
    });

    expect(sessionStore["agent:main:main"]?.abortedLastRun).toBe(true);
    expect(state.persistSessionEntryMock).toHaveBeenCalledWith(
      expect.objectContaining({
        entry: expect.objectContaining({
          restartRecoveryDeliveryContext: {
            channel: "discord",
            to: "discord:dm:123",
            accountId: "main",
          },
          restartRecoveryDeliveryRunId: "session-1",
        }),
      }),
    );
  });

  it("does not recreate a deleted session entry during restart recovery cleanup", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeEmptyResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    const sessionStore: Record<string, SessionEntry> = { "agent:main:main": sessionEntry };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockImplementation(async () => {
      delete sessionStore["agent:main:main"];
      return { deliverySucceeded: true };
    });

    await agentCommand({
      message: "hello",
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
      deliver: true,
    });

    expect(sessionStore["agent:main:main"]).toBeUndefined();
  });

  it("does not clear restart recovery context from a rotated session entry", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeEmptyResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    const rotatedEntry: SessionEntry = {
      sessionId: "session-2",
      updatedAt: 2,
      restartRecoveryDeliveryContext: {
        channel: "discord",
        to: "discord:dm:456",
        accountId: "main",
      },
    };
    const sessionStore: Record<string, SessionEntry> = { "agent:main:main": sessionEntry };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockImplementation(async () => {
      sessionStore["agent:main:main"] = rotatedEntry;
      return { deliverySucceeded: true };
    });

    await agentCommand({
      message: "hello",
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
      deliver: true,
    });

    expect(sessionStore["agent:main:main"]).toEqual(rotatedEntry);
  });

  it("does not clear restart recovery context from another active run in the same session", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeEmptyResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    const laterRunEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 2,
      restartRecoveryDeliveryContext: {
        channel: "discord",
        to: "discord:dm:456",
        accountId: "main",
      },
      restartRecoveryDeliveryRunId: "later-run",
    };
    const sessionStore: Record<string, SessionEntry> = { "agent:main:main": sessionEntry };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockImplementation(async () => {
      sessionStore["agent:main:main"] = laterRunEntry;
      return { deliverySucceeded: false };
    });

    await agentCommand({
      message: "hello",
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
      deliver: true,
    });

    expect(sessionStore["agent:main:main"]).toEqual(laterRunEntry);
  });

  it("stores pending final delivery with the current run delivery context", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
    };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = { "agent:main:main": sessionEntry };
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockResolvedValue({ deliverySucceeded: false });

    await agentCommand({
      message: "hello",
      channel: "discord",
      to: "discord:dm:123",
      accountId: "main",
      deliver: true,
    });

    const pendingEntries = state.persistSessionEntryMock.mock.calls
      .map((call) => (call[0] as { entry?: SessionEntry }).entry)
      .filter((entry): entry is SessionEntry => entry?.pendingFinalDelivery === true);
    expect(pendingEntries).toContainEqual(
      expect.objectContaining({
        pendingFinalDeliveryText: "ok",
        pendingFinalDeliveryContext: {
          channel: "discord",
          to: "discord:dm:123",
          accountId: "main",
        },
      }),
    );
  });

  it("clears stale flag-only pending final delivery when there is no final payload", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeEmptyResult("openai", "gpt-5.4"));

    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
      pendingFinalDelivery: true,
      pendingFinalDeliveryCreatedAt: 2,
      pendingFinalDeliveryLastAttemptAt: 3,
      pendingFinalDeliveryAttemptCount: 4,
      pendingFinalDeliveryLastError: "previous failure",
      pendingFinalDeliveryContext: { channel: "tui" },
      pendingFinalDeliveryIntentId: "intent-1",
    };
    state.sessionEntryMock = sessionEntry;
    state.sessionStoreMock = { "agent:main:main": sessionEntry };
    state.storePathMock = "/tmp/openclaw-sessions.json";
    state.deliverAgentCommandResultMock.mockResolvedValue(undefined);

    await agentCommand({
      message: "hello",
      to: "+1234567890",
      deliver: true,
    });

    expect(state.persistSessionEntryMock).toHaveBeenCalledWith(
      expect.objectContaining({
        entry: expect.objectContaining({
          pendingFinalDelivery: undefined,
          pendingFinalDeliveryText: undefined,
          pendingFinalDeliveryCreatedAt: undefined,
          pendingFinalDeliveryLastAttemptAt: undefined,
          pendingFinalDeliveryAttemptCount: undefined,
          pendingFinalDeliveryLastError: undefined,
          pendingFinalDeliveryContext: undefined,
          pendingFinalDeliveryIntentId: undefined,
        }),
      }),
    );
  });

  it("keeps internal session-effect CLI runs out of visible session state", async () => {
    setupSingleAttemptFallback();
    const visibleEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: 1,
      sessionFile: "/tmp/session.jsonl",
      providerOverride: "anthropic",
      modelOverride: "claude",
      modelOverrideSource: "user",
      skillsSnapshot: { prompt: "visible", skills: [{ name: "existing" }], version: 1 },
    };
    const sessionStore: Record<string, SessionEntry> = { "agent:main:main": visibleEntry };
    state.sessionEntryMock = visibleEntry;
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-session-store.json";
    const attemptCalls: Array<{ sessionFile?: string; sessionEntry?: SessionEntry }> = [];
    state.runAgentAttemptMock.mockImplementation(async (params) => {
      attemptCalls.push(params as { sessionFile?: string; sessionEntry?: SessionEntry });
      return makeSuccessResult("openai", "gpt-5.4");
    });

    await agentCommand({
      message: "internal resume",
      to: "+1234567890",
      sessionEffects: "internal",
      suppressPromptPersistence: true,
    });

    expect(state.prepareInternalSessionEffectsTranscriptMock).toHaveBeenCalledWith({
      sessionFile: "/tmp/session.jsonl",
      runId: expect.any(String),
    });
    expect(attemptCalls).toHaveLength(1);
    expect(attemptCalls[0]?.sessionFile).toBe("/tmp/openclaw-internal-run.jsonl");
    expect(attemptCalls[0]?.sessionEntry).toStrictEqual(visibleEntry);
    expect(state.persistSessionEntryMock).not.toHaveBeenCalled();
    expect(state.updateSessionStoreAfterAgentRunMock).not.toHaveBeenCalled();
    expect(sessionStore["agent:main:main"]).toBe(visibleEntry);
  });

  it("does not duplicate finishing lifecycle when an attempt already emitted finishing", async () => {
    setupModelSwitchRetry({
      provider: "openai",
      model: "gpt-5.4",
    });
    state.runAgentAttemptMock.mockImplementation(async (attemptParams: unknown) => {
      state.emitAgentEventMock({
        runId: "run-live-switch",
        stream: "lifecycle",
        data: { phase: "finishing" },
      });
      (attemptParams as { onAgentEvent?: (evt: unknown) => void }).onAgentEvent?.({
        stream: "lifecycle",
        data: { phase: "finishing" },
      });
      return makeSuccessResult("openai", "gpt-5.4");
    });

    await runBasicAgentCommand();

    const lifecycleFinishingCalls = state.emitAgentEventMock.mock.calls.filter(
      (call: unknown[]) => {
        const arg = call[0] as { stream?: string; data?: { phase?: string } };
        return arg?.stream === "lifecycle" && arg?.data?.phase === "finishing";
      },
    );
    expect(lifecycleFinishingCalls).toHaveLength(1);
  });

  it("validates explicit thinking against configured model compat without an allowlist", async () => {
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: { primary: "gmn/gpt-5.4" },
        },
      },
      models: {
        providers: {
          gmn: {
            models: [
              {
                id: "gpt-5.4",
                name: "GPT 5.4 via GMN",
                reasoning: true,
                compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
              },
            ],
          },
        },
      },
    };
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      const result = await params.run(params.provider, params.model);
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [],
      };
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("gmn", "gpt-5.4"));

    await agentCommand({
      message: "hello",
      to: "+1234567890",
      thinking: "xhigh",
    });

    const thinkingArgs = requireRecord(
      mockCallArg(state.isThinkingLevelSupportedMock),
      "thinking args",
    );
    expect(thinkingArgs.provider).toBe("gmn");
    expect(thinkingArgs.model).toBe("gpt-5.4");
    expect(thinkingArgs.level).toBe("xhigh");
    const catalog = requireArray(thinkingArgs.catalog, "thinking catalog");
    expectRecordFields(catalog[0], {
      provider: "gmn",
      id: "gpt-5.4",
      compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
    });
  });

  it("validates explicit thinking against allowlisted configured model compat when manifest catalog is empty", async () => {
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: { primary: "gmn/gpt-5.4" },
          models: {
            "gmn/gpt-5.4": {},
          },
        },
      },
      models: {
        providers: {
          gmn: {
            models: [
              {
                id: "gpt-5.4",
                name: "GPT 5.4 via GMN",
                reasoning: true,
                compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
              },
            ],
          },
        },
      },
    };
    state.loadManifestModelCatalogMock.mockReturnValue([]);
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      const result = await params.run(params.provider, params.model);
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [],
      };
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("gmn", "gpt-5.4"));

    await agentCommand({
      message: "hello",
      to: "+1234567890",
      thinking: "xhigh",
    });

    expect(state.loadManifestModelCatalogMock).toHaveBeenCalledTimes(1);
    const thinkingArgs = requireRecord(
      mockCallArg(state.isThinkingLevelSupportedMock),
      "thinking args",
    );
    expect(thinkingArgs.provider).toBe("gmn");
    expect(thinkingArgs.model).toBe("gpt-5.4");
    expect(thinkingArgs.level).toBe("xhigh");
    const catalog = requireArray(thinkingArgs.catalog, "thinking catalog");
    expectRecordFields(catalog[0], {
      provider: "gmn",
      id: "gpt-5.4",
      compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
    });
  });

  it("resolves explicit model aliases before thinking validation", async () => {
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          model: { primary: "openai/gpt-5.4" },
          models: {
            "openai/*": {},
            "codex/gpt-5.5": {
              alias: "code",
            },
          },
        },
      },
      models: {
        providers: {
          codex: {
            models: [
              {
                id: "gpt-5.5",
                name: "GPT 5.5 Codex",
                reasoning: true,
                compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
              },
            ],
          },
        },
      },
    };
    state.loadManifestModelCatalogMock.mockReturnValue([]);
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      const result = await params.run(params.provider, params.model);
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [],
      };
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("codex", "gpt-5.5"));

    await agentCommand({
      message: "hello",
      to: "+1234567890",
      model: "code",
      thinking: "xhigh",
      allowModelOverride: true,
    });

    const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams;
    expect(fallbackParams.provider).toBe("codex");
    expect(fallbackParams.model).toBe("gpt-5.5");
    const thinkingArgs = requireRecord(
      mockCallArg(state.isThinkingLevelSupportedMock),
      "thinking args",
    );
    expect(thinkingArgs.provider).toBe("codex");
    expect(thinkingArgs.model).toBe("gpt-5.5");
    expect(thinkingArgs.level).toBe("xhigh");
  });

  it("records fallback steps to the session trajectory runtime", async () => {
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      await params.onFallbackStep?.({
        fallbackStepType: "fallback_step",
        fallbackStepFromModel: "ollama/llama3",
        fallbackStepToModel: "openai/gpt-5.4",
        fallbackStepFromFailureReason: "overloaded",
        fallbackStepChainPosition: 1,
        fallbackStepFinalOutcome: "next_fallback",
      });
      const result = await params.run(params.provider, params.model);
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [],
      };
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

    await runBasicAgentCommand();

    expect(state.trajectoryRecordEventMock).toHaveBeenCalledTimes(1);
    expect(mockCallArg(state.trajectoryRecordEventMock, 0, 0)).toBe("model.fallback_step");
    expectRecordFields(mockCallArg(state.trajectoryRecordEventMock, 0, 1), {
      fallbackStepType: "fallback_step",
      fallbackStepFromModel: "ollama/llama3",
      fallbackStepToModel: "openai/gpt-5.4",
      fallbackStepFromFailureReason: "overloaded",
      fallbackStepChainPosition: 1,
      fallbackStepFinalOutcome: "next_fallback",
    });
    expect(state.trajectoryFlushMock).toHaveBeenCalledTimes(1);
  });

  it("suppresses duplicate user persistence only after the current turn has flushed", async () => {
    type AttemptCall = {
      onUserMessagePersisted?: () => void;
      suppressPromptPersistenceOnRetry?: boolean;
    };
    const attemptCalls: AttemptCall[] = [];
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      const first = await params.run(params.provider, params.model);
      const result = await params.run(params.provider, params.model);
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [first],
      };
    });
    state.runAgentAttemptMock.mockImplementation(async (attemptParams: AttemptCall) => {
      const firstAttempt = attemptCalls.length === 0;
      attemptCalls.push(attemptParams);
      if (firstAttempt) {
        if (!attemptParams.onUserMessagePersisted) {
          throw new Error("expected retry persistence callback on first attempt");
        }
        attemptParams.onUserMessagePersisted();
      } else {
        attemptParams.onUserMessagePersisted?.();
      }
      return makeSuccessResult("openai", "gpt-5.4");
    });

    await runBasicAgentCommand();

    expect(attemptCalls).toHaveLength(2);
    expect(attemptCalls[0]?.suppressPromptPersistenceOnRetry).not.toBe(true);
    expect(attemptCalls[1]?.suppressPromptPersistenceOnRetry).toBe(true);
  });

  it("keeps a hook-blocked user turn suppressed across model fallback", async () => {
    type AttemptCall = {
      suppressPromptPersistenceOnRetry?: boolean;
      userTurnTranscriptRecorder?: {
        markBlocked: () => void;
      };
    };
    const attemptCalls: AttemptCall[] = [];
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      const first = await params.run(params.provider, params.model);
      const result = await params.run(params.provider, params.model);
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [first],
      };
    });
    state.runAgentAttemptMock.mockImplementation(async (attemptParams: AttemptCall) => {
      attemptCalls.push(attemptParams);
      if (attemptCalls.length === 1) {
        attemptParams.userTurnTranscriptRecorder?.markBlocked();
      }
      return makeSuccessResult("openai", "gpt-5.4");
    });

    await runBasicAgentCommand();

    expect(attemptCalls).toHaveLength(2);
    expect(attemptCalls[1]?.userTurnTranscriptRecorder).toBe(
      attemptCalls[0]?.userTurnTranscriptRecorder,
    );
    expect(attemptCalls[0]?.suppressPromptPersistenceOnRetry).toBe(false);
    expect(attemptCalls[1]?.suppressPromptPersistenceOnRetry).toBe(true);
  });

  it("suppresses prompt persistence for internal handoffs on every fallback attempt", async () => {
    type AttemptCall = {
      suppressPromptPersistenceOnRetry?: boolean;
    };
    const attemptCalls: AttemptCall[] = [];
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      const first = await params.run(params.provider, params.model);
      const result = await params.run(params.provider, params.model);
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [first],
      };
    });
    state.runAgentAttemptMock.mockImplementation(async (attemptParams: AttemptCall) => {
      attemptCalls.push(attemptParams);
      const result = makeSuccessResult("openai", "gpt-5.4") as ReturnType<
        typeof makeSuccessResult
      > & {
        meta: Record<string, unknown> & { executionTrace?: Record<string, unknown> };
      };
      result.meta.executionTrace = {
        runner: "cli",
        fallbackUsed: false,
        winnerProvider: "openai",
        winnerModel: "gpt-5.4",
      };
      return result;
    });

    await agentCommand({
      message: "internal handoff",
      to: "+1234567890",
      suppressPromptPersistence: true,
    });

    expect(attemptCalls).toHaveLength(2);
    expect(attemptCalls[0]?.suppressPromptPersistenceOnRetry).toBe(true);
    expect(attemptCalls[1]?.suppressPromptPersistenceOnRetry).toBe(true);
    expectRecordFields(mockCallArg(state.persistCliTurnTranscriptMock), {
      skipUserTurn: true,
    });
  });

  it("preserves an explicit empty transcript message as user-turn omission", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

    await agentCommand({
      message: "synthetic announce prompt",
      transcriptMessage: "",
      to: "+1234567890",
    });

    const attempt = mockCallArg(state.runAgentAttemptMock) as {
      suppressPromptPersistenceOnRetry?: boolean;
      userTurnTranscriptRecorder?: { message?: unknown };
    };
    expect(attempt.suppressPromptPersistenceOnRetry).toBe(true);
    expect(attempt.userTurnTranscriptRecorder?.message).toBeUndefined();
  });

  it("uses a tracker-only recorder for text plus image turns", async () => {
    setupSingleAttemptFallback();
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

    await agentCommand({
      message: "inspect this image",
      transcriptMessage: "canonical image caption",
      images: [{ type: "image", data: "aGVsbG8=", mimeType: "image/png" }],
      to: "+1234567890",
    });

    const attempt = mockCallArg(state.runAgentAttemptMock) as {
      transcriptBody?: string;
      suppressPromptPersistenceOnRetry?: boolean;
      userTurnTranscriptRecorder?: { message?: unknown };
    };
    expect(attempt.transcriptBody).toBe("canonical image caption");
    expect(attempt.suppressPromptPersistenceOnRetry).toBe(false);
    expect(attempt.userTurnTranscriptRecorder?.message).toMatchObject({
      role: "user",
      content: "canonical image caption",
    });
  });

  it("propagates non-switch errors without retrying and emits lifecycle error", async () => {
    state.runWithModelFallbackMock.mockRejectedValueOnce(new Error("provider down"));

    await expect(
      agentCommand({
        message: "hello",
        to: "+1234567890",
      }),
    ).rejects.toThrow("provider down");

    expect(state.runWithModelFallbackMock).toHaveBeenCalledTimes(1);

    const lifecycleErrorCalls = state.emitAgentEventMock.mock.calls.filter((call: unknown[]) => {
      const arg = call[0] as { stream?: string; data?: { phase?: string } };
      return arg?.stream === "lifecycle" && arg?.data?.phase === "error";
    });
    expect(lifecycleErrorCalls.length).toBeGreaterThanOrEqual(1);
  });

  it("marks lifecycle errors aborted when cancellation reaches post-turn handling", async () => {
    const abortController = new AbortController();
    state.runWithModelFallbackMock.mockImplementationOnce(async () => {
      abortController.abort();
      throw new Error("request aborted");
    });

    await expect(
      agentCommand({
        message: "hello",
        to: "+1234567890",
        abortSignal: abortController.signal,
      }),
    ).rejects.toThrow("request aborted");

    expect(
      state.emitAgentEventMock.mock.calls.some(([event]) => {
        const candidate = event as {
          stream?: string;
          data?: { phase?: string; aborted?: boolean };
        };
        return (
          candidate.stream === "lifecycle" &&
          candidate.data?.phase === "error" &&
          candidate.data.aborted === true
        );
      }),
    ).toBe(true);
  });

  it("marks direct active-run cancellation aborted without a caller signal", async () => {
    state.runWithModelFallbackMock.mockRejectedValueOnce(createAgentRunDirectAbortError());

    await expect(
      agentCommand({
        message: "hello",
        to: "+1234567890",
      }),
    ).rejects.toThrow("agent run aborted");

    expect(
      state.emitAgentEventMock.mock.calls.some(([event]) => {
        const candidate = event as {
          stream?: string;
          data?: { phase?: string; aborted?: boolean; stopReason?: string };
        };
        return (
          candidate.stream === "lifecycle" &&
          candidate.data?.phase === "error" &&
          candidate.data.aborted === true &&
          candidate.data.stopReason === "aborted"
        );
      }),
    ).toBe(true);
  });

  it("propagates authProfileId from the switch error to the retried session entry", async () => {
    let capturedAuthProfileProvider: string | undefined;
    setupModelSwitchRetry({
      provider: "openai",
      model: "gpt-5.4",
      authProfileId: "profile-openai-prod",
      authProfileIdSource: "user",
    });

    state.runAgentAttemptMock.mockImplementation(async (...args: unknown[]) => {
      const attemptParams = args[0] as { authProfileProvider?: string } | undefined;
      capturedAuthProfileProvider = attemptParams?.authProfileProvider;
      return makeSuccessResult("openai", "gpt-5.4");
    });

    await runBasicAgentCommand();

    expect(capturedAuthProfileProvider).toBe("openai");
    expect(state.runWithModelFallbackMock).toHaveBeenCalledTimes(2);
  });

  it("does not persist a user live switch as an auto fallback probe result", async () => {
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: Date.now(),
      providerOverride: "openai",
      modelOverride: "claude",
      modelOverrideSource: "auto",
      modelOverrideFallbackOriginProvider: "anthropic",
      modelOverrideFallbackOriginModel: "claude",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    state.sessionEntryMock = sessionEntry;
    const sessionStore: Record<string, SessionEntry> = { "agent:main:main": sessionEntry };
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-session-store.json";
    setupModelSwitchRetry({
      provider: "openai",
      model: "gpt-5.4",
      authProfileId: "openai:primary",
      authProfileIdSource: "user",
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

    await runBasicAgentCommand();

    const autoPinnedSwitchWrites = state.persistSessionEntryMock.mock.calls.filter((call) => {
      const entry = (call[0] as { entry?: Record<string, unknown> } | undefined)?.entry;
      return (
        entry?.providerOverride === "openai" &&
        entry?.modelOverride === "gpt-5.4" &&
        entry?.modelOverrideSource === "auto" &&
        entry?.modelOverrideFallbackOriginProvider === "anthropic"
      );
    });
    expect(autoPinnedSwitchWrites).toHaveLength(0);
    expectRecordFields(mockCallArg(state.updateSessionStoreAfterAgentRunMock), {
      fallbackProvider: "openai",
      fallbackModel: "gpt-5.4",
    });
  });

  it("does not overwrite a concurrent user model switch after a primary probe", async () => {
    const sessionEntry: SessionEntry = {
      sessionId: "session-1",
      updatedAt: Date.now(),
      providerOverride: "openai",
      modelOverride: "claude",
      modelOverrideSource: "auto",
      modelOverrideFallbackOriginProvider: "anthropic",
      modelOverrideFallbackOriginModel: "claude",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    };
    state.sessionEntryMock = sessionEntry;
    const sessionStore: Record<string, SessionEntry> = { "agent:main:main": sessionEntry };
    state.sessionStoreMock = sessionStore;
    state.storePathMock = "/tmp/openclaw-session-store.json";
    state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => {
      const result = await params.run(params.provider, params.model);
      sessionStore["agent:main:main"] = {
        sessionId: "session-1",
        updatedAt: Date.now(),
        providerOverride: "google",
        modelOverride: "gemini-3-pro",
        modelOverrideSource: "user",
        skillsSnapshot: { prompt: "", skills: [], version: 0 },
      };
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [],
      };
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude"));

    await runBasicAgentCommand();

    expectRecordFields(sessionStore["agent:main:main"], {
      providerOverride: "google",
      modelOverride: "gemini-3-pro",
      modelOverrideSource: "user",
    });
  });

  it("keeps aliased session auth profiles for codex-cli runs", async () => {
    let capturedAuthProfileProvider: string | undefined;
    const sessionEntry = {
      sessionId: "session-1",
      updatedAt: Date.now(),
      providerOverride: "codex-cli",
      modelOverride: "gpt-5.4",
      authProfileOverride: "openai:work",
      authProfileOverrideSource: "user",
      skillsSnapshot: { prompt: "", skills: [], version: 0 },
    } satisfies SessionEntry;
    state.sessionEntryMock = sessionEntry;
    state.runtimeConfigMock = {
      agents: {
        defaults: {
          models: {
            "codex-cli/gpt-5.4": {},
          },
        },
      },
    };
    state.authProfileStoreMock = {
      profiles: {
        "openai:work": {
          type: "api_key",
          provider: "openai",
          key: "sk-test",
        },
      },
    };
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      const result = await params.run(params.provider, params.model);
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [],
      };
    });
    state.runAgentAttemptMock.mockImplementation(async (...args: unknown[]) => {
      const attemptParams = args[0] as { authProfileProvider?: string } | undefined;
      capturedAuthProfileProvider = attemptParams?.authProfileProvider;
      return makeSuccessResult("codex-cli", "gpt-5.4");
    });

    await runBasicAgentCommand();

    expect(capturedAuthProfileProvider).toBe("codex-cli");
    expect(state.clearSessionAuthProfileOverrideMock).not.toHaveBeenCalled();
  });

  it("hydrates stripped persisted skill snapshots before running the CLI path", async () => {
    const persistedSnapshot = {
      prompt: "persisted prompt",
      skills: [{ name: "cli-skill" }],
      skillFilter: ["cli-skill"],
      version: 0,
    };
    const rebuiltSkills = [
      {
        name: "cli-skill",
        description: "CLI skill",
        filePath: "/tmp/workspace/skills/cli-skill/SKILL.md",
        baseDir: "/tmp/workspace/skills/cli-skill",
        source: "# CLI skill",
      },
    ];
    state.sessionEntryMock = {
      sessionId: "session-1",
      updatedAt: Date.now(),
      skillsSnapshot: persistedSnapshot,
    };
    state.buildWorkspaceSkillSnapshotMock.mockReturnValue({
      prompt: "rebuilt prompt",
      skills: [{ name: "different-skill" }],
      resolvedSkills: rebuiltSkills,
      version: 99,
    });
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      const result = await params.run(params.provider, params.model);
      return {
        result,
        provider: params.provider,
        model: params.model,
        attempts: [],
      };
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude"));

    await runBasicAgentCommand();

    const attemptParams = mockCallArg(state.runAgentAttemptMock) as {
      skillsSnapshot?: Record<string, unknown>;
    };
    expectRecordFields(attemptParams?.skillsSnapshot, {
      prompt: "persisted prompt",
      skills: [{ name: "cli-skill" }],
      skillFilter: ["cli-skill"],
      version: 0,
      resolvedSkills: rebuiltSkills,
    });
    expect(state.buildWorkspaceSkillSnapshotMock).toHaveBeenCalledTimes(1);
  });

  it("classifies empty embedded run results before model fallback accepts them", async () => {
    let observedClassification: unknown;
    state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
      const primaryResult = await params.run(params.provider, params.model);
      observedClassification = await params.classifyResult?.({
        provider: params.provider,
        model: params.model,
        result: primaryResult,
        attempt: 1,
        total: 2,
      });
      const fallbackResult = await params.run("openai", "gpt-5.4");
      return {
        result: fallbackResult,
        provider: "openai",
        model: "gpt-5.4",
        attempts: [
          {
            provider: params.provider,
            model: params.model,
            error: "empty result",
            reason: "format",
            code: "empty_result",
          },
        ],
      };
    });
    state.runAgentAttemptMock
      .mockResolvedValueOnce(makeEmptyResult("anthropic", "claude"))
      .mockResolvedValueOnce(makeSuccessResult("openai", "gpt-5.4"));

    await runBasicAgentCommand();

    expectRecordFields(observedClassification, {
      reason: "format",
      code: "empty_result",
    });
    expect(state.runAgentAttemptMock).toHaveBeenCalledTimes(2);
    expectRecordFields(mockCallArg(state.runAgentAttemptMock, 1), {
      providerOverride: "openai",
      modelOverride: "gpt-5.4",
      isFallbackRetry: true,
    });
    const deliveryParams = requireRecord(
      mockCallArg(state.deliverAgentCommandResultMock),
      "delivery params",
    );
    const result = requireRecord(deliveryParams.result, "delivery result");
    const meta = requireRecord(result.meta, "delivery result meta");
    const agentMeta = requireRecord(meta.agentMeta, "delivery agent meta");
    const fallbackAttempts = requireArray(agentMeta.fallbackAttempts, "fallback attempts");
    expectRecordFields(fallbackAttempts[0], {
      provider: "anthropic",
      model: "claude",
      reason: "format",
    });
  });

  it("emits a failure lifecycle after delivering a preserved exhausted result", async () => {
    const exhaustedResult = {
      payloads: [{ text: "Terminal tool summary", isError: true }],
      meta: {
        durationMs: 100,
        aborted: false,
        stopReason: "end_turn",
        error: {
          kind: "incomplete_turn",
          message: "All fallback candidates ended incomplete",
          fallbackSafe: true,
          terminalPresentation: true,
        },
        agentMeta: { provider: "anthropic", model: "claude" },
      },
    };
    state.runAgentAttemptMock.mockImplementationOnce(async (attemptParams: unknown) => {
      const params = attemptParams as {
        deferTerminalLifecycle?: boolean;
        onAgentEvent?: (event: { stream: string; data: Record<string, unknown> }) => void;
      };
      expect(params.deferTerminalLifecycle).toBe(true);
      params.onAgentEvent?.({
        stream: "lifecycle",
        data: {
          phase: "finishing",
          error: "All fallback candidates ended incomplete",
        },
      });
      return exhaustedResult;
    });
    state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
      outcome: "exhausted",
      result: await params.run("anthropic", "claude"),
      provider: "anthropic",
      model: "claude",
      attempts: [
        {
          provider: "anthropic",
          model: "claude",
          error: "All fallback candidates ended incomplete",
          reason: "format",
        },
      ],
    }));

    await runBasicAgentCommand();

    expect(state.deliverAgentCommandResultMock).toHaveBeenCalledTimes(1);
    const lifecycleEvents = state.emitAgentEventMock.mock.calls
      .map((call) => call[0] as { stream?: string; data?: Record<string, unknown> })
      .filter((event) => event.stream === "lifecycle");
    expect(lifecycleEvents.some((event) => event.data?.phase === "finishing")).toBe(false);
    expect(lifecycleEvents.some((event) => event.data?.phase === "end")).toBe(false);
    expect(lifecycleEvents).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          data: expect.objectContaining({
            phase: "error",
            error: "All fallback candidates ended incomplete",
            fallbackExhaustedFailure: true,
          }),
        }),
      ]),
    );
  });

  it("emits a failure lifecycle for completed non-fallbackable error results", async () => {
    const terminalErrorResult = {
      payloads: [{ text: "Command may have changed state", isError: true }],
      meta: {
        durationMs: 100,
        aborted: false,
        stopReason: "end_turn",
        replayInvalid: true,
        error: {
          kind: "incomplete_turn",
          message: "raw provider detail should stay private",
          fallbackSafe: false,
        },
        agentMeta: { provider: "anthropic", model: "claude" },
      },
    };
    state.runAgentAttemptMock.mockImplementationOnce(async (attemptParams: unknown) => {
      const params = attemptParams as {
        onAgentEvent?: (event: { stream: string; data: Record<string, unknown> }) => void;
      };
      params.onAgentEvent?.({
        stream: "lifecycle",
        data: {
          phase: "finishing",
          error: "Command may have changed state",
          replayInvalid: true,
        },
      });
      return terminalErrorResult;
    });
    state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
      outcome: "completed",
      result: await params.run("anthropic", "claude"),
      provider: "anthropic",
      model: "claude",
      attempts: [],
    }));

    await runBasicAgentCommand();

    expect(state.deliverAgentCommandResultMock).toHaveBeenCalledTimes(1);
    const lifecycleEvents = state.emitAgentEventMock.mock.calls
      .map((call) => call[0] as { stream?: string; data?: Record<string, unknown> })
      .filter((event) => event.stream === "lifecycle");
    expect(lifecycleEvents).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          data: expect.objectContaining({
            phase: "error",
            error: "Command may have changed state",
            replayInvalid: true,
          }),
        }),
      ]),
    );
    expect(
      lifecycleEvents.some(
        (event) => event.data?.phase === "end" || event.data?.fallbackExhaustedFailure === true,
      ),
    ).toBe(false);
    expect(JSON.stringify(lifecycleEvents)).not.toContain("raw provider detail");
  });

  it("updates hasSessionModelOverride for fallback resolution after switch", async () => {
    setupModelSwitchRetry({
      provider: "openai",
      model: "gpt-5.4",
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));

    state.resolveEffectiveModelFallbacksMock.mockClear();

    await runBasicAgentCommand();

    expectFallbackOverrideCalls(false, true);
  });

  it("does not flip hasSessionModelOverride on auth-only switch with same model", async () => {
    setupModelSwitchRetry({
      provider: "anthropic",
      model: "claude",
      authProfileId: "profile-99",
      authProfileIdSource: "user",
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude"));

    state.resolveEffectiveModelFallbacksMock.mockClear();

    await runBasicAgentCommand();

    expectFallbackOverrideCalls(false, false);
  });

  it("sends internal completion wakes to ACP sessions as plain prompt text", async () => {
    state.acpResolveSessionMock.mockReturnValue({
      kind: "ready",
      meta: {
        agent: "claude",
        cwd: "/tmp/workspace",
      },
    });

    await agentCommand({
      message: [
        INTERNAL_RUNTIME_CONTEXT_BEGIN,
        "OpenClaw runtime context (internal):",
        "hidden task completion event",
        INTERNAL_RUNTIME_CONTEXT_END,
      ].join("\n"),
      sessionKey: "agent:main:main",
      internalEvents: [
        {
          type: "task_completion",
          source: "subagent",
          childSessionKey: "agent:main:subagent:child",
          childSessionId: "child-session-id",
          announceType: "subagent task",
          taskLabel: "inspect ACP delivery",
          status: "ok",
          statusLabel: "completed successfully",
          result: "child output",
          replyInstruction: "Summarize the result for the user.",
        },
      ],
    });

    expect(state.acpRunTurnMock).toHaveBeenCalledTimes(1);
    const runTurnParams = mockCallArg(state.acpRunTurnMock) as { text?: string };
    expect(runTurnParams.text).toContain("A background task completed.");
    expect(runTurnParams.text).toContain("inspect ACP delivery");
    expect(runTurnParams.text).toContain("child output");
    expect(runTurnParams.text).not.toContain(INTERNAL_RUNTIME_CONTEXT_BEGIN);
    expect(runTurnParams.text).not.toContain(INTERNAL_RUNTIME_CONTEXT_END);

    expect(state.persistAcpTurnTranscriptMock).toHaveBeenCalledTimes(1);
    const transcriptParams = mockCallArg(state.persistAcpTurnTranscriptMock) as {
      body?: string;
      transcriptBody?: string;
    };
    expect(transcriptParams.body).toBe(runTurnParams.text);
    expect(transcriptParams.transcriptBody).toContain("A background task completed.");
    expect(transcriptParams.transcriptBody).not.toContain(INTERNAL_RUNTIME_CONTEXT_BEGIN);
    expect(transcriptParams.transcriptBody).not.toContain(INTERNAL_RUNTIME_CONTEXT_END);
  });

  it("allows manual ACP spawn turns when ACP dispatch is disabled", async () => {
    state.acpResolveSessionMock.mockReturnValue({
      kind: "ready",
      meta: {
        agent: "claude",
        cwd: "/tmp/workspace",
      },
    });
    state.resolveAcpDispatchPolicyErrorMock.mockReturnValue(
      new Error("ACP dispatch is disabled by policy (`acp.dispatch.enabled=false`)."),
    );

    await agentCommand({
      message: "bootstrap ACP child",
      sessionKey: "agent:main:main",
      acpTurnSource: "manual_spawn",
    });

    expect(state.resolveAcpExplicitTurnPolicyErrorMock).toHaveBeenCalledTimes(1);
    expect(state.resolveAcpDispatchPolicyErrorMock).not.toHaveBeenCalled();
    expect(state.acpRunTurnMock).toHaveBeenCalledTimes(1);
  });

  it("keeps ordinary ACP turns blocked when ACP dispatch is disabled", async () => {
    state.acpResolveSessionMock.mockReturnValue({
      kind: "ready",
      meta: {
        agent: "claude",
        cwd: "/tmp/workspace",
      },
    });
    state.resolveAcpDispatchPolicyErrorMock.mockReturnValue(
      new Error("ACP dispatch is disabled by policy (`acp.dispatch.enabled=false`)."),
    );

    await expect(
      agentCommand({
        message: "automatic ACP turn",
        sessionKey: "agent:main:main",
      }),
    ).rejects.toThrow("ACP dispatch is disabled");

    expect(state.resolveAcpExplicitTurnPolicyErrorMock).not.toHaveBeenCalled();
    expect(state.resolveAcpDispatchPolicyErrorMock).toHaveBeenCalledTimes(1);
    expect(state.acpRunTurnMock).not.toHaveBeenCalled();
  });

  it("flips hasSessionModelOverride on provider-only switch with same model", async () => {
    setupModelSwitchRetry({
      provider: "openai",
      model: "claude",
    });
    state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "claude"));

    state.resolveEffectiveModelFallbacksMock.mockClear();

    await runBasicAgentCommand();

    expectFallbackOverrideCalls(false, true);
  });
});
