import fs from "node:fs";
import path from "node:path";
import type { SessionsListParams } from "../../packages/gateway-protocol/src/index.js";
import { resolveModelAgentRuntimeMetadata } from "../agents/agent-runtime-metadata.js";
import {
  listAgentIds,
  resolveAgentConfig,
  resolveAgentEffectiveModelPrimary,
  resolveAgentModelFallbacksOverride,
  resolveAgentWorkspaceDir,
  resolveDefaultAgentId,
} from "../agents/agent-scope.js";
import { lookupContextTokens, resolveContextTokensForModel } from "../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import {
  findModelCatalogEntry,
  modelSupportsInput,
  type ModelCatalogEntry,
} from "../agents/model-catalog.js";
import {
  inferUniqueProviderFromConfiguredModels,
  isCliProvider,
  normalizeStoredOverrideModel,
  parseModelRef,
  resolveConfiguredModelRef,
  resolveDefaultModelForAgent,
  resolvePersistedSelectedModelRef,
  resolveThinkingDefault,
} from "../agents/model-selection.js";
import {
  buildSubagentRunReadIndex,
  countActiveDescendantRuns,
  getSessionDisplaySubagentRunByChildSessionKey,
  getSubagentSessionRuntimeMs,
  getSubagentSessionStartedAt,
  isSubagentRunLive,
  listSubagentRunsForController,
  resolveSubagentSessionStatus,
} from "../agents/subagent-registry-read.js";
import {
  RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS,
  shouldKeepSubagentRunChildLink,
} from "../agents/subagent-run-liveness.js";
import { listThinkingLevelOptions } from "../auto-reply/thinking.js";
import { getRuntimeConfig } from "../config/io.js";
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
import { resolveStateDir } from "../config/paths.js";
import {
  buildGroupDisplayName,
  getSessionStoreCacheVersion,
  loadSessionStore,
  resolveAllAgentSessionStoreTargetsSync,
  resolveAgentMainSessionKey,
  resolveFreshSessionTotalTokens,
  resolveSessionGoalDisplayState,
  resolveStorePath,
  type SessionEntry,
  type SessionStoreTarget,
  type SessionScope,
} from "../config/sessions.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { openRootFileSync } from "../infra/boundary-file-read.js";
import { projectPluginSessionExtensionsSync } from "../plugins/host-hook-state.js";
import {
  DEFAULT_AGENT_ID,
  normalizeAgentId,
  normalizeMainKey,
  parseAgentSessionKey,
} from "../routing/session-key.js";
import { isCronRunSessionKey } from "../sessions/session-key-utils.js";
import {
  AVATAR_MAX_BYTES,
  isAvatarDataUrl,
  isAvatarHttpUrl,
  isPathWithinRoot,
  isWorkspaceRelativeAvatarPath,
  resolveAvatarMime,
} from "../shared/avatar-policy.js";
import {
  normalizeLowercaseStringOrEmpty,
  normalizeOptionalString,
  normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import { uniqueStrings } from "../shared/string-normalization.js";
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.shared.js";
import type { ModelCostConfig } from "../utils/usage-format.js";
import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js";
import {
  resolveSessionStoreAgentId,
  resolveSessionStoreKey,
  resolveStoredSessionKeyForAgentStore,
} from "./session-store-key.js";
import {
  readRecentSessionUsageFromTranscript,
  readSessionTitleFieldsFromTranscriptAsync,
  readSessionTitleFieldsFromTranscript,
} from "./session-utils.fs.js";
import type {
  GatewayAgentRow,
  GatewaySessionRow,
  GatewaySessionsDefaults,
  SessionRunStatus,
  SessionsListResult,
} from "./session-utils.types.js";

export {
  archiveFileOnDisk,
  archiveSessionTranscripts,
  attachOpenClawTranscriptMeta,
  capArrayByJsonBytes,
  readFirstUserMessageFromTranscript,
  readLatestSessionUsageFromTranscriptAsync,
  readLatestRecentSessionUsageFromTranscriptAsync,
  readRecentSessionUsageFromTranscriptAsync,
  readRecentSessionMessagesAsync,
  readRecentSessionMessagesWithStatsAsync,
  readRecentSessionTranscriptLines,
  readRecentSessionUsageFromTranscript,
  readSessionMessageCountAsync,
  readSessionTitleFieldsFromTranscript,
  readSessionTitleFieldsFromTranscriptAsync,
  readSessionPreviewItemsFromTranscript,
  readSessionMessagesAsync,
  visitSessionMessagesAsync,
  resolveSessionTranscriptCandidates,
} from "./session-utils.fs.js";
export type { ReadSessionMessagesAsyncOptions } from "./session-utils.fs.js";
export { canonicalizeSpawnedByForAgent, resolveSessionStoreKey } from "./session-store-key.js";
export type {
  GatewayAgentRow,
  GatewaySessionRow,
  GatewaySessionsDefaults,
  SessionsListResult,
  SessionsPatchResult,
  SessionsPreviewEntry,
  SessionsPreviewResult,
} from "./session-utils.types.js";

const DERIVED_TITLE_MAX_LEN = 60;

function tryResolveExistingPath(value: string): string | null {
  try {
    return fs.realpathSync(value);
  } catch {
    return null;
  }
}

function resolveIdentityAvatarUrl(
  cfg: OpenClawConfig,
  agentId: string,
  avatar: string | undefined,
): string | undefined {
  if (!avatar) {
    return undefined;
  }
  const trimmed = normalizeOptionalString(avatar) ?? "";
  if (!trimmed) {
    return undefined;
  }
  if (isAvatarDataUrl(trimmed) || isAvatarHttpUrl(trimmed)) {
    return trimmed;
  }
  if (!isWorkspaceRelativeAvatarPath(trimmed)) {
    return undefined;
  }
  const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
  const workspaceRoot = tryResolveExistingPath(workspaceDir) ?? path.resolve(workspaceDir);
  const resolvedCandidate = path.resolve(workspaceRoot, trimmed);
  if (!isPathWithinRoot(workspaceRoot, resolvedCandidate)) {
    return undefined;
  }
  try {
    const opened = openRootFileSync({
      absolutePath: resolvedCandidate,
      rootPath: workspaceRoot,
      rootRealPath: workspaceRoot,
      boundaryLabel: "workspace root",
      maxBytes: AVATAR_MAX_BYTES,
      skipLexicalRootCheck: true,
    });
    if (!opened.ok) {
      return undefined;
    }
    try {
      const buffer = fs.readFileSync(opened.fd);
      const mime = resolveAvatarMime(resolvedCandidate);
      return `data:${mime};base64,${buffer.toString("base64")}`;
    } finally {
      fs.closeSync(opened.fd);
    }
  } catch {
    return undefined;
  }
}

function formatSessionIdPrefix(sessionId: string, updatedAt?: number | null): string {
  const prefix = sessionId.slice(0, 8);
  if (updatedAt && updatedAt > 0) {
    const d = new Date(updatedAt);
    const date = d.toISOString().slice(0, 10);
    return `${prefix} (${date})`;
  }
  return prefix;
}

function truncateTitle(text: string, maxLen: number): string {
  if (text.length <= maxLen) {
    return text;
  }
  const cut = text.slice(0, maxLen - 1);
  const lastSpace = cut.lastIndexOf(" ");
  if (lastSpace > maxLen * 0.6) {
    return cut.slice(0, lastSpace) + "…";
  }
  return cut + "…";
}

export function deriveSessionTitle(
  entry: SessionEntry | undefined,
  firstUserMessage?: string | null,
): string | undefined {
  if (!entry) {
    return undefined;
  }

  if (normalizeOptionalString(entry.displayName)) {
    return normalizeOptionalString(entry.displayName);
  }

  if (normalizeOptionalString(entry.subject)) {
    return normalizeOptionalString(entry.subject);
  }

  if (firstUserMessage?.trim()) {
    const normalized = firstUserMessage.replace(/\s+/g, " ").trim();
    return truncateTitle(normalized, DERIVED_TITLE_MAX_LEN);
  }

  if (entry.sessionId) {
    return formatSessionIdPrefix(entry.sessionId, entry.updatedAt);
  }

  return undefined;
}

function resolveSessionRuntimeMs(
  run: { startedAt?: number; endedAt?: number; accumulatedRuntimeMs?: number } | null,
  now: number,
) {
  return getSubagentSessionRuntimeMs(run, now);
}

function resolvePositiveNumber(value: number | null | undefined): number | undefined {
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
}

function resolveNonNegativeNumber(value: number | null | undefined): number | undefined {
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
}

type SessionCompactionCheckpointEntry = NonNullable<SessionEntry["compactionCheckpoints"]>[number];

function isProjectableCompactionCheckpoint(
  value: unknown,
): value is SessionCompactionCheckpointEntry {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return false;
  }
  const checkpoint = value as {
    checkpointId?: unknown;
    createdAt?: unknown;
    reason?: unknown;
  };
  return (
    Boolean(normalizeOptionalString(checkpoint.checkpointId)) &&
    typeof checkpoint.createdAt === "number" &&
    Number.isFinite(checkpoint.createdAt) &&
    (checkpoint.reason === "manual" ||
      checkpoint.reason === "auto-threshold" ||
      checkpoint.reason === "overflow-retry" ||
      checkpoint.reason === "timeout-retry")
  );
}

function resolveProjectableCompactionCheckpoints(
  entry?: Pick<SessionEntry, "compactionCheckpoints"> | null,
): SessionCompactionCheckpointEntry[] {
  const checkpoints = entry?.compactionCheckpoints;
  if (!Array.isArray(checkpoints) || checkpoints.length === 0) {
    return [];
  }
  return checkpoints.filter(isProjectableCompactionCheckpoint);
}

function resolveLatestCompactionCheckpoint(
  checkpoints: readonly SessionCompactionCheckpointEntry[],
): SessionCompactionCheckpointEntry | undefined {
  return checkpoints.reduce<SessionCompactionCheckpointEntry | undefined>(
    (latest, checkpoint) =>
      !latest || checkpoint.createdAt > latest.createdAt ? checkpoint : latest,
    undefined,
  );
}

function buildCompactionCheckpointPreview(
  checkpoint: SessionCompactionCheckpointEntry | undefined,
): GatewaySessionRow["latestCompactionCheckpoint"] {
  if (!checkpoint) {
    return undefined;
  }
  const checkpointId = normalizeOptionalString(checkpoint.checkpointId);
  const createdAt = checkpoint.createdAt;
  const reason = checkpoint.reason;
  if (!checkpointId || typeof createdAt !== "number" || !Number.isFinite(createdAt)) {
    return undefined;
  }
  if (
    reason !== "manual" &&
    reason !== "auto-threshold" &&
    reason !== "overflow-retry" &&
    reason !== "timeout-retry"
  ) {
    return undefined;
  }
  return {
    checkpointId,
    createdAt,
    reason,
  };
}

function resolveModelCostConfigCached(
  provider: string | undefined,
  model: string | undefined,
  cfg: OpenClawConfig,
  rowContext?: SessionListRowContext,
): ModelCostConfig | undefined {
  if (!rowContext) {
    return resolveModelCostConfig({ provider, model, config: cfg });
  }
  const key = createSessionRowModelCacheKey(provider, model);
  if (rowContext.modelCostConfigByModelRef.has(key)) {
    return rowContext.modelCostConfigByModelRef.get(key);
  }
  const value = resolveModelCostConfig({ provider, model, config: cfg });
  rowContext.modelCostConfigByModelRef.set(key, value);
  return value;
}

function resolveEstimatedSessionCostUsd(params: {
  cfg: OpenClawConfig;
  provider?: string;
  model?: string;
  entry?: Pick<
    SessionEntry,
    "estimatedCostUsd" | "inputTokens" | "outputTokens" | "cacheRead" | "cacheWrite"
  >;
  explicitCostUsd?: number;
  rowContext?: SessionListRowContext;
}): number | undefined {
  const explicitCostUsd = resolveNonNegativeNumber(
    params.explicitCostUsd ?? params.entry?.estimatedCostUsd,
  );
  if (explicitCostUsd !== undefined) {
    return explicitCostUsd;
  }
  const input = resolvePositiveNumber(params.entry?.inputTokens);
  const output = resolvePositiveNumber(params.entry?.outputTokens);
  const cacheRead = resolvePositiveNumber(params.entry?.cacheRead);
  const cacheWrite = resolvePositiveNumber(params.entry?.cacheWrite);
  if (
    input === undefined &&
    output === undefined &&
    cacheRead === undefined &&
    cacheWrite === undefined
  ) {
    return undefined;
  }
  const cost = resolveModelCostConfigCached(
    params.provider,
    params.model,
    params.cfg,
    params.rowContext,
  );
  if (!cost) {
    return undefined;
  }
  const estimated = estimateUsageCost({
    usage: {
      ...(input !== undefined ? { input } : {}),
      ...(output !== undefined ? { output } : {}),
      ...(cacheRead !== undefined ? { cacheRead } : {}),
      ...(cacheWrite !== undefined ? { cacheWrite } : {}),
    },
    cost,
  });
  return resolveNonNegativeNumber(estimated);
}

const STALE_STORE_ONLY_CHILD_LINK_MS = 60 * 60 * 1_000;
const SINGLE_ROW_CONTEXT_CACHE_MAX_ENTRIES = 64;

function isFinitePositiveTimestamp(value: unknown): value is number {
  return typeof value === "number" && Number.isFinite(value) && value > 0;
}

function isTerminalSessionStatus(status: unknown): status is Exclude<SessionRunStatus, "running"> {
  return status === "done" || status === "failed" || status === "killed" || status === "timeout";
}

function shouldKeepStoreOnlyChildLink(entry: SessionEntry, now: number): boolean {
  if (isTerminalSessionStatus(entry.status) || isFinitePositiveTimestamp(entry.endedAt)) {
    const endedAt = isFinitePositiveTimestamp(entry.endedAt) ? entry.endedAt : entry.updatedAt;
    return (
      isFinitePositiveTimestamp(endedAt) && now - endedAt <= RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS
    );
  }
  if (entry.status === "running" || isFinitePositiveTimestamp(entry.startedAt)) {
    return true;
  }
  return (
    isFinitePositiveTimestamp(entry.updatedAt) &&
    now - entry.updatedAt <= STALE_STORE_ONLY_CHILD_LINK_MS
  );
}

type SessionListRowContext = {
  subagentRuns: ReturnType<typeof buildSubagentRunReadIndex>;
  storeChildSessionsByKey: Map<string, string[]>;
  selectedModelByOverrideRef: Map<string, ReturnType<typeof resolveSessionModelRef>>;
  // Per-list memoization for deterministic resolvers that scale linearly with
  // session count but only depend on (provider, model[, agentId]). Sessions
  // in a single list typically share a small set of those tuples, so caching
  // here collapses the work to O(unique tuples) per call.
  thinkingMetadataByModelRef: Map<
    string,
    {
      levels: ReturnType<typeof listThinkingLevelOptions>;
      defaultLevel: ReturnType<typeof resolveGatewaySessionThinkingDefault>;
    }
  >;
  displayModelIdentityByKey: Map<string, { provider?: string; model?: string }>;
  modelCostConfigByModelRef: Map<string, ModelCostConfig | undefined>;
};

type SingleRowChildSessionCandidateCacheEntry = {
  store: Record<string, SessionEntry>;
  storeVersion: number;
  childSessionCandidatesByParentKey: Map<string, string[]>;
};

const singleRowChildSessionCandidateCache = new Map<
  string,
  SingleRowChildSessionCandidateCacheEntry
>();

function rememberSingleRowChildSessionCandidateCacheEntry(
  storePath: string,
  entry: SingleRowChildSessionCandidateCacheEntry,
) {
  if (singleRowChildSessionCandidateCache.has(storePath)) {
    singleRowChildSessionCandidateCache.delete(storePath);
  }
  singleRowChildSessionCandidateCache.set(storePath, entry);
  if (singleRowChildSessionCandidateCache.size <= SINGLE_ROW_CONTEXT_CACHE_MAX_ENTRIES) {
    return;
  }
  const oldestKey = singleRowChildSessionCandidateCache.keys().next().value;
  if (oldestKey) {
    singleRowChildSessionCandidateCache.delete(oldestKey);
  }
}

function buildStoreChildSessionCandidateIndex(
  store: Record<string, SessionEntry>,
): Map<string, string[]> {
  const childSessionsByKey = new Map<string, string[]>();
  for (const [key, entry] of Object.entries(store)) {
    if (!entry) {
      continue;
    }
    const parentKeys = [
      normalizeOptionalString(entry.spawnedBy),
      normalizeOptionalString(entry.parentSessionKey),
    ].filter((value): value is string => Boolean(value) && value !== key);
    for (const parentKey of parentKeys) {
      addChildSessionKey(childSessionsByKey, parentKey, key);
    }
  }
  return childSessionsByKey;
}

function getSingleRowChildSessionCandidates(params: {
  storePath: string;
  store: Record<string, SessionEntry>;
}): Map<string, string[]> {
  const storeVersion = getSessionStoreCacheVersion(params.storePath);
  const cached = singleRowChildSessionCandidateCache.get(params.storePath);
  if (cached && cached.store === params.store && cached.storeVersion === storeVersion) {
    return cached.childSessionCandidatesByParentKey;
  }
  const childSessionCandidatesByParentKey = buildStoreChildSessionCandidateIndex(params.store);
  rememberSingleRowChildSessionCandidateCacheEntry(params.storePath, {
    store: params.store,
    storeVersion,
    childSessionCandidatesByParentKey,
  });
  return childSessionCandidatesByParentKey;
}

function resolveRuntimeChildSessionKeys(
  controllerSessionKey: string,
  now = Date.now(),
  subagentRuns?: SessionListRowContext["subagentRuns"],
): string[] | undefined {
  const childSessionKeys = new Set<string>();
  const controllerKey = controllerSessionKey.trim();
  const runs = subagentRuns
    ? (subagentRuns.runsByControllerSessionKey.get(controllerKey) ?? [])
    : listSubagentRunsForController(controllerSessionKey);
  for (const entry of runs) {
    const childSessionKey = normalizeOptionalString(entry.childSessionKey);
    if (!childSessionKey) {
      continue;
    }
    const latest = subagentRuns
      ? subagentRuns.getDisplaySubagentRun(childSessionKey)
      : getSessionDisplaySubagentRunByChildSessionKey(childSessionKey);
    if (!latest) {
      continue;
    }
    const latestControllerSessionKey =
      normalizeOptionalString(latest?.controllerSessionKey) ||
      normalizeOptionalString(latest?.requesterSessionKey);
    if (latestControllerSessionKey !== controllerSessionKey) {
      continue;
    }
    if (
      !shouldKeepSubagentRunChildLink(latest, {
        activeDescendants: subagentRuns
          ? subagentRuns.countActiveDescendantRuns(childSessionKey)
          : countActiveDescendantRuns(childSessionKey),
        now,
      })
    ) {
      continue;
    }
    childSessionKeys.add(childSessionKey);
  }
  const childSessions = Array.from(childSessionKeys);
  return childSessions.length > 0 ? childSessions : undefined;
}

function addChildSessionKey(
  childSessionsByKey: Map<string, string[]>,
  parentKey: string,
  childKey: string,
) {
  const current = childSessionsByKey.get(parentKey);
  if (current) {
    if (!current.includes(childKey)) {
      current.push(childKey);
    }
    return;
  }
  childSessionsByKey.set(parentKey, [childKey]);
}

function buildStoreChildSessionIndex(
  store: Record<string, SessionEntry>,
  now = Date.now(),
  subagentRuns?: SessionListRowContext["subagentRuns"],
): Map<string, string[]> {
  const childSessionsByKey = new Map<string, string[]>();
  for (const [key, entry] of Object.entries(store)) {
    if (!entry) {
      continue;
    }
    const parentKeys = [
      normalizeOptionalString(entry.spawnedBy),
      normalizeOptionalString(entry.parentSessionKey),
    ].filter((value): value is string => Boolean(value) && value !== key);
    if (parentKeys.length === 0) {
      continue;
    }
    const latest = subagentRuns
      ? subagentRuns.getDisplaySubagentRun(key)
      : getSessionDisplaySubagentRunByChildSessionKey(key);
    let latestControllerSessionKey: string | undefined;
    if (latest) {
      latestControllerSessionKey =
        normalizeOptionalString(latest.controllerSessionKey) ||
        normalizeOptionalString(latest.requesterSessionKey);
      if (
        !shouldKeepSubagentRunChildLink(latest, {
          activeDescendants: subagentRuns
            ? subagentRuns.countActiveDescendantRuns(key)
            : countActiveDescendantRuns(key),
          now,
        })
      ) {
        continue;
      }
    } else if (!shouldKeepStoreOnlyChildLink(entry, now)) {
      continue;
    }
    for (const parentKey of parentKeys) {
      if (latestControllerSessionKey && latestControllerSessionKey !== parentKey) {
        continue;
      }
      addChildSessionKey(childSessionsByKey, parentKey, key);
    }
  }
  return childSessionsByKey;
}

function resolveStoreChildSessionKeysFromCandidates(params: {
  store: Record<string, SessionEntry>;
  key: string;
  now: number;
  candidates: ReadonlyMap<string, readonly string[]>;
}): string[] | undefined {
  const childSessionKeys: string[] = [];
  for (const childKey of params.candidates.get(params.key) ?? []) {
    const entry = params.store[childKey];
    if (!entry) {
      continue;
    }
    const latest = getSessionDisplaySubagentRunByChildSessionKey(childKey);
    if (latest) {
      const latestControllerSessionKey =
        normalizeOptionalString(latest.controllerSessionKey) ||
        normalizeOptionalString(latest.requesterSessionKey);
      if (latestControllerSessionKey !== params.key) {
        continue;
      }
      if (
        !shouldKeepSubagentRunChildLink(latest, {
          activeDescendants: countActiveDescendantRuns(childKey),
          now: params.now,
        })
      ) {
        continue;
      }
      childSessionKeys.push(childKey);
      continue;
    }
    if (!shouldKeepStoreOnlyChildLink(entry, params.now)) {
      continue;
    }
    childSessionKeys.push(childKey);
  }
  return childSessionKeys.length > 0 ? childSessionKeys : undefined;
}

function buildSessionListRowContext(params: {
  store: Record<string, SessionEntry>;
  now: number;
}): SessionListRowContext {
  const subagentRuns = buildSubagentRunReadIndex(params.now);
  return {
    subagentRuns,
    storeChildSessionsByKey: buildStoreChildSessionIndex(params.store, params.now, subagentRuns),
    selectedModelByOverrideRef: new Map(),
    thinkingMetadataByModelRef: new Map(),
    displayModelIdentityByKey: new Map(),
    modelCostConfigByModelRef: new Map(),
  };
}

function buildSingleRowStoreChildSessionsByKey(params: {
  store: Record<string, SessionEntry>;
  storePath: string;
  key: string;
  now: number;
}): Map<string, string[]> {
  const storeChildSessions = resolveStoreChildSessionKeysFromCandidates({
    store: params.store,
    key: params.key,
    now: params.now,
    candidates: getSingleRowChildSessionCandidates({
      storePath: params.storePath,
      store: params.store,
    }),
  });
  return storeChildSessions ? new Map([[params.key, storeChildSessions]]) : new Map();
}

function createSessionRowModelCacheKey(provider: string | undefined, model: string | undefined) {
  return `${normalizeLowercaseStringOrEmpty(provider)}\0${normalizeOptionalString(model) ?? ""}`;
}

function resolveSessionSelectedModelRef(params: {
  cfg: OpenClawConfig;
  entry?: SessionEntry;
  agentId: string;
  rowContext?: SessionListRowContext;
  allowPluginNormalization?: boolean;
}): ReturnType<typeof resolveSessionModelRef> | null {
  const override = normalizeStoredOverrideModel({
    providerOverride: params.entry?.providerOverride,
    modelOverride: params.entry?.modelOverride,
  });
  if (!override.modelOverride) {
    return null;
  }
  if (!params.rowContext) {
    return resolveSessionModelRef(params.cfg, params.entry, params.agentId, {
      allowPluginNormalization: params.allowPluginNormalization,
    });
  }
  const key = [
    normalizeAgentId(params.agentId),
    override.providerOverride ?? "",
    override.modelOverride,
  ].join("\0");
  const cached = params.rowContext.selectedModelByOverrideRef.get(key);
  if (cached) {
    return cached;
  }
  const selected = resolveSessionModelRef(params.cfg, params.entry, params.agentId, {
    allowPluginNormalization: params.allowPluginNormalization,
  });
  params.rowContext.selectedModelByOverrideRef.set(key, selected);
  return selected;
}

function resolveSessionRowThinkingMetadata(params: {
  cfg: OpenClawConfig;
  agentId: string;
  provider: string;
  model: string;
  modelCatalog?: ModelCatalogEntry[];
  rowContext?: SessionListRowContext;
}): {
  levels: ReturnType<typeof listThinkingLevelOptions>;
  defaultLevel: ReturnType<typeof resolveGatewaySessionThinkingDefault>;
} {
  if (!params.rowContext) {
    return {
      levels: listThinkingLevelOptions(params.provider, params.model, params.modelCatalog),
      defaultLevel: resolveGatewaySessionThinkingDefault({
        cfg: params.cfg,
        provider: params.provider,
        model: params.model,
        agentId: params.agentId,
        modelCatalog: params.modelCatalog,
      }),
    };
  }
  const key = `${normalizeAgentId(params.agentId)}\0${createSessionRowModelCacheKey(
    params.provider,
    params.model,
  )}`;
  const cached = params.rowContext.thinkingMetadataByModelRef.get(key);
  if (cached) {
    return cached;
  }
  const metadata = {
    levels: listThinkingLevelOptions(params.provider, params.model, params.modelCatalog),
    defaultLevel: resolveGatewaySessionThinkingDefault({
      cfg: params.cfg,
      provider: params.provider,
      model: params.model,
      agentId: params.agentId,
      modelCatalog: params.modelCatalog,
    }),
  };
  params.rowContext.thinkingMetadataByModelRef.set(key, metadata);
  return metadata;
}

function mergeChildSessionKeys(
  runtimeChildSessions: string[] | undefined,
  storeChildSessions: string[] | undefined,
): string[] | undefined {
  if (!runtimeChildSessions?.length) {
    return storeChildSessions?.length ? storeChildSessions : undefined;
  }
  if (!storeChildSessions?.length) {
    return runtimeChildSessions;
  }
  return uniqueStrings([...runtimeChildSessions, ...storeChildSessions]);
}

function resolveChildSessionKeys(
  controllerSessionKey: string,
  store: Record<string, SessionEntry>,
  now = Date.now(),
  subagentRuns?: SessionListRowContext["subagentRuns"],
): string[] | undefined {
  const runtimeChildSessions = resolveRuntimeChildSessionKeys(
    controllerSessionKey,
    now,
    subagentRuns,
  );
  const storeChildSessions = buildStoreChildSessionIndex(store, now, subagentRuns).get(
    controllerSessionKey,
  );
  return mergeChildSessionKeys(runtimeChildSessions, storeChildSessions);
}

function resolveTranscriptUsageFallback(params: {
  cfg: OpenClawConfig;
  key: string;
  entry?: SessionEntry;
  storePath: string;
  fallbackProvider?: string;
  fallbackModel?: string;
  maxTranscriptBytes?: number;
  rowContext?: SessionListRowContext;
  agentId?: string;
}): {
  estimatedCostUsd?: number;
  totalTokens?: number;
  totalTokensFresh?: boolean;
  contextTokens?: number;
  modelProvider?: string;
  model?: string;
} | null {
  const entry = params.entry;
  if (!entry?.sessionId) {
    return null;
  }
  const parsed = parseAgentSessionKey(params.key);
  const agentId = parsed?.agentId
    ? normalizeAgentId(parsed.agentId)
    : normalizeAgentId(params.agentId ?? resolveDefaultAgentId(params.cfg));
  const snapshot = readRecentSessionUsageFromTranscript(
    entry.sessionId,
    params.storePath,
    entry.sessionFile,
    agentId,
    typeof params.maxTranscriptBytes === "number" ? params.maxTranscriptBytes : 256 * 1024,
  );
  if (!snapshot) {
    return null;
  }
  const modelProvider = snapshot.modelProvider ?? params.fallbackProvider;
  const model = snapshot.model ?? params.fallbackModel;
  const contextTokens = resolveContextTokensForModel({
    cfg: params.cfg,
    provider: modelProvider,
    model,
    // Gateway/session listing is read-only; don't start async model discovery.
    allowAsyncLoad: false,
  });
  const estimatedCostUsd = resolveEstimatedSessionCostUsd({
    cfg: params.cfg,
    provider: modelProvider,
    model,
    explicitCostUsd: snapshot.costUsd,
    entry: {
      inputTokens: snapshot.inputTokens,
      outputTokens: snapshot.outputTokens,
      cacheRead: snapshot.cacheRead,
      cacheWrite: snapshot.cacheWrite,
    },
    rowContext: params.rowContext,
  });
  return {
    modelProvider,
    model,
    totalTokens: resolvePositiveNumber(snapshot.totalTokens),
    totalTokensFresh: snapshot.totalTokensFresh === true,
    contextTokens: resolvePositiveNumber(contextTokens),
    estimatedCostUsd,
  };
}

/**
 * Returns the owning agent id if the session key belongs to an agent that is no
 * longer present in config (deleted). Returns null for non-agent legacy/global
 * keys, or when the owning agent still exists (#65524).
 */
export function resolveDeletedAgentIdFromSessionKey(
  cfg: OpenClawConfig,
  sessionKey: string,
): string | null {
  const parsed = parseAgentSessionKey(sessionKey);
  if (!parsed) {
    return null;
  }
  const agentId = normalizeAgentId(parsed.agentId);
  if (listAgentIds(cfg).includes(agentId)) {
    return null;
  }
  return agentId;
}

export function loadSessionEntry(sessionKey: string, opts?: { agentId?: string; clone?: boolean }) {
  const cfg = getRuntimeConfig();
  const key = normalizeOptionalString(sessionKey) ?? "";
  const target = resolveGatewaySessionStoreTarget({
    cfg,
    key,
    ...(opts?.clone === false ? { clone: false } : {}),
    ...(opts?.agentId ? { agentId: opts.agentId } : {}),
  });
  const storePath = target.storePath;
  const store = loadSessionStore(storePath, opts?.clone === false ? { clone: false } : undefined);
  const freshestMatch = resolveFreshestSessionStoreMatchFromStoreKeys(store, target.storeKeys);
  const legacyKey = freshestMatch?.key !== target.canonicalKey ? freshestMatch?.key : undefined;
  return {
    cfg,
    storePath,
    store,
    entry: freshestMatch?.entry,
    canonicalKey: target.canonicalKey,
    legacyKey,
  };
}

export function resolveFreshestSessionStoreMatchFromStoreKeys(
  store: Record<string, SessionEntry>,
  storeKeys: string[],
): { key: string; entry: SessionEntry } | undefined {
  let freshest: { key: string; entry: SessionEntry } | undefined;
  for (const key of storeKeys) {
    const entry = store[key];
    if (!entry) {
      continue;
    }
    const match = { key, entry };
    if (!freshest || (match.entry.updatedAt ?? 0) > (freshest.entry.updatedAt ?? 0)) {
      freshest = match;
    }
  }
  return freshest;
}

export function resolveFreshestSessionEntryFromStoreKeys(
  store: Record<string, SessionEntry>,
  storeKeys: string[],
): SessionEntry | undefined {
  return resolveFreshestSessionStoreMatchFromStoreKeys(store, storeKeys)?.entry;
}

function findFreshestStoreMatch(
  store: Record<string, SessionEntry>,
  ...candidates: string[]
): { entry: SessionEntry; key: string } | undefined {
  const matches = new Map<string, { entry: SessionEntry; key: string }>();
  for (const candidate of candidates) {
    const trimmed = normalizeOptionalString(candidate) ?? "";
    if (!trimmed) {
      continue;
    }
    const exact = store[trimmed];
    if (exact) {
      matches.set(trimmed, { entry: exact, key: trimmed });
    }
    for (const key of findStoreKeysIgnoreCase(store, trimmed)) {
      const entry = store[key];
      if (entry) {
        matches.set(key, { entry, key });
      }
    }
  }
  if (matches.size === 0) {
    return undefined;
  }
  let freshest: { entry: SessionEntry; key: string } | undefined;
  for (const match of matches.values()) {
    if (!freshest || (match.entry.updatedAt ?? 0) > (freshest.entry.updatedAt ?? 0)) {
      freshest = match;
    }
  }
  return freshest;
}

/**
 * Find all on-disk store keys that match the given key case-insensitively.
 * Returns every key from the store whose lowercased form equals the target's lowercased form.
 */
export function findStoreKeysIgnoreCase(
  store: Record<string, unknown>,
  targetKey: string,
): string[] {
  const lowered = normalizeLowercaseStringOrEmpty(targetKey);
  const matches: string[] = [];
  for (const key of Object.keys(store)) {
    if (normalizeLowercaseStringOrEmpty(key) === lowered) {
      matches.push(key);
    }
  }
  return matches;
}

/**
 * Remove legacy key variants for one canonical session key.
 * Candidates can include aliases (for example, "agent:ops:main" when canonical is "agent:ops:work").
 */
export function pruneLegacyStoreKeys(params: {
  store: Record<string, unknown>;
  canonicalKey: string;
  candidates: Iterable<string>;
}) {
  const keysToDelete = new Set<string>();
  for (const candidate of params.candidates) {
    const trimmed = normalizeOptionalString(candidate ?? "") ?? "";
    if (!trimmed) {
      continue;
    }
    if (trimmed !== params.canonicalKey) {
      keysToDelete.add(trimmed);
    }
    for (const match of findStoreKeysIgnoreCase(params.store, trimmed)) {
      if (match !== params.canonicalKey) {
        keysToDelete.add(match);
      }
    }
  }
  for (const key of keysToDelete) {
    delete params.store[key];
  }
}

export function migrateAndPruneGatewaySessionStoreKey(params: {
  cfg: OpenClawConfig;
  key: string;
  store: Record<string, SessionEntry>;
  agentId?: string;
}) {
  const target = resolveGatewaySessionStoreTarget({
    cfg: params.cfg,
    key: params.key,
    store: params.store,
    ...(params.agentId ? { agentId: params.agentId } : {}),
  });
  const primaryKey = target.canonicalKey;
  const freshestMatch = resolveFreshestSessionStoreMatchFromStoreKeys(
    params.store,
    target.storeKeys,
  );
  if (freshestMatch) {
    const currentPrimary = params.store[primaryKey];
    if (!currentPrimary || (freshestMatch.entry.updatedAt ?? 0) > (currentPrimary.updatedAt ?? 0)) {
      params.store[primaryKey] = freshestMatch.entry;
    }
  }
  pruneLegacyStoreKeys({
    store: params.store,
    canonicalKey: primaryKey,
    candidates: target.storeKeys,
  });
  return { target, primaryKey, entry: params.store[primaryKey] };
}

export function classifySessionKey(key: string, entry?: SessionEntry): GatewaySessionRow["kind"] {
  if (key === "global") {
    return "global";
  }
  if (key === "unknown") {
    return "unknown";
  }
  if (entry?.chatType === "group" || entry?.chatType === "channel") {
    return "group";
  }
  if (key.includes(":group:") || key.includes(":channel:")) {
    return "group";
  }
  return "direct";
}

export function parseGroupKey(
  key: string,
): { channel?: string; kind?: "group" | "channel"; id?: string } | null {
  const agentParsed = parseAgentSessionKey(key);
  const rawKey = agentParsed?.rest ?? key;
  const parts = rawKey.split(":").filter(Boolean);
  if (parts.length >= 3) {
    const [channel, kind, ...rest] = parts;
    if (kind === "group" || kind === "channel") {
      const id = rest.join(":");
      return { channel, kind, id };
    }
  }
  return null;
}

function isStorePathTemplate(store?: string): boolean {
  return typeof store === "string" && store.includes("{agentId}");
}

function listExistingAgentIdsFromDisk(): string[] {
  const root = resolveStateDir();
  const agentsDir = path.join(root, "agents");
  try {
    const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
    return entries
      .filter((entry) => entry.isDirectory())
      .map((entry) => normalizeAgentId(entry.name))
      .filter(Boolean);
  } catch {
    return [];
  }
}

function listConfiguredAgentIds(cfg: OpenClawConfig): string[] {
  const ids = new Set<string>();
  const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
  ids.add(defaultId);

  for (const entry of cfg.agents?.list ?? []) {
    if (entry?.id) {
      ids.add(normalizeAgentId(entry.id));
    }
  }

  for (const id of listExistingAgentIdsFromDisk()) {
    ids.add(id);
  }

  const sorted = Array.from(ids).filter(Boolean);
  sorted.sort((a, b) => a.localeCompare(b));
  return sorted.includes(defaultId)
    ? [defaultId, ...sorted.filter((id) => id !== defaultId)]
    : sorted;
}

function normalizeFallbackList(values: readonly string[]): string[] {
  const out: string[] = [];
  const seen = new Set<string>();
  for (const value of values) {
    const trimmed = value.trim();
    if (!trimmed) {
      continue;
    }
    const key = normalizeLowercaseStringOrEmpty(trimmed);
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    out.push(trimmed);
  }
  return out;
}

function resolveGatewayAgentModel(
  cfg: OpenClawConfig,
  agentId: string,
): GatewayAgentRow["model"] | undefined {
  const primary = resolveAgentEffectiveModelPrimary(cfg, agentId)?.trim();
  const fallbackOverride = resolveAgentModelFallbacksOverride(cfg, agentId);
  const defaultFallbacks = resolveAgentModelFallbackValues(cfg.agents?.defaults?.model);
  const fallbacks = normalizeFallbackList(fallbackOverride ?? defaultFallbacks);
  if (!primary && fallbacks.length === 0) {
    return undefined;
  }
  return {
    ...(primary ? { primary } : {}),
    ...(fallbacks.length > 0 ? { fallbacks } : {}),
  };
}

export function listAgentsForGateway(cfg: OpenClawConfig): {
  defaultId: string;
  mainKey: string;
  scope: SessionScope;
  agents: GatewayAgentRow[];
} {
  const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
  const mainKey = normalizeMainKey(cfg.session?.mainKey);
  const scope = cfg.session?.scope ?? "per-sender";
  const configuredById = new Map<
    string,
    { name?: string; identity?: GatewayAgentRow["identity"] }
  >();
  for (const entry of cfg.agents?.list ?? []) {
    if (!entry?.id) {
      continue;
    }
    const configuredName = normalizeOptionalString(entry.name);
    const identity = entry.identity
      ? {
          name: normalizeOptionalString(entry.identity.name),
          theme: normalizeOptionalString(entry.identity.theme),
          emoji: normalizeOptionalString(entry.identity.emoji),
          avatar: normalizeOptionalString(entry.identity.avatar),
          avatarUrl: resolveIdentityAvatarUrl(
            cfg,
            normalizeAgentId(entry.id),
            normalizeOptionalString(entry.identity.avatar),
          ),
        }
      : undefined;
    configuredById.set(normalizeAgentId(entry.id), {
      name: configuredName ?? identity?.name,
      identity,
    });
  }
  const explicitIds = new Set(
    (cfg.agents?.list ?? [])
      .map((entry) => (entry?.id ? normalizeAgentId(entry.id) : ""))
      .filter(Boolean),
  );
  const allowedIds = explicitIds.size > 0 ? new Set([...explicitIds, defaultId]) : null;
  let agentIds = listConfiguredAgentIds(cfg).filter((id) =>
    allowedIds ? allowedIds.has(id) : true,
  );
  if (mainKey && !agentIds.includes(mainKey) && (!allowedIds || allowedIds.has(mainKey))) {
    agentIds = [...agentIds, mainKey];
  }
  const agents = agentIds.map((id) => {
    const meta = configuredById.get(id);
    const model = resolveGatewayAgentModel(cfg, id);
    const resolvedModel = resolveDefaultModelForAgent({ cfg, agentId: id });
    return Object.assign(
      {
        id,
        name: meta?.name,
        identity: meta?.identity,
        workspace: resolveAgentWorkspaceDir(cfg, id),
        agentRuntime: resolveModelAgentRuntimeMetadata({
          cfg,
          agentId: id,
          provider: resolvedModel.provider,
          model: resolvedModel.model,
          sessionKey: resolveAgentMainSessionKey({ cfg, agentId: id }),
          acpRuntime: false,
        }),
      },
      model ? { model } : {},
    );
  });
  return { defaultId, mainKey, scope, agents };
}

function buildGatewaySessionStoreScanTargets(params: {
  cfg: OpenClawConfig;
  key: string;
  canonicalKey: string;
  agentId: string;
}): string[] {
  const targets = new Set<string>();
  if (params.canonicalKey) {
    targets.add(params.canonicalKey);
  }
  if (params.key && params.key !== params.canonicalKey) {
    targets.add(params.key);
  }
  if (params.canonicalKey === "global" || params.canonicalKey === "unknown") {
    return [...targets];
  }
  const agentMainKey = resolveAgentMainSessionKey({ cfg: params.cfg, agentId: params.agentId });
  if (params.canonicalKey === agentMainKey) {
    targets.add(`agent:${params.agentId}:main`);
  }
  return [...targets];
}

function resolveGatewaySessionStoreCandidates(
  cfg: OpenClawConfig,
  agentId: string,
): SessionStoreTarget[] {
  const storeConfig = cfg.session?.store;
  const defaultTarget = {
    agentId,
    storePath: resolveStorePath(storeConfig, { agentId }),
  };
  if (!isStorePathTemplate(storeConfig)) {
    return [defaultTarget];
  }
  const targets = new Map<string, SessionStoreTarget>();
  targets.set(defaultTarget.storePath, defaultTarget);
  for (const target of resolveAllAgentSessionStoreTargetsSync(cfg)) {
    if (target.agentId === agentId) {
      targets.set(target.storePath, target);
    }
  }
  return [...targets.values()];
}

function resolveGatewaySessionStoreLookup(params: {
  cfg: OpenClawConfig;
  key: string;
  canonicalKey: string;
  agentId: string;
  clone?: boolean;
  initialStore?: Record<string, SessionEntry>;
}): {
  storePath: string;
  store: Record<string, SessionEntry>;
  match: { entry: SessionEntry; key: string } | undefined;
} {
  const scanTargets = buildGatewaySessionStoreScanTargets(params);
  const candidates = resolveGatewaySessionStoreCandidates(params.cfg, params.agentId);
  const fallback = candidates[0] ?? {
    agentId: params.agentId,
    storePath: resolveStorePath(params.cfg.session?.store, { agentId: params.agentId }),
  };
  const loadOptions = params.clone === false ? { clone: false } : undefined;
  let selectedStorePath = fallback.storePath;
  let selectedStore = params.initialStore ?? loadSessionStore(fallback.storePath, loadOptions);
  let selectedMatch = findFreshestStoreMatch(selectedStore, ...scanTargets);
  let selectedUpdatedAt = selectedMatch?.entry.updatedAt ?? Number.NEGATIVE_INFINITY;

  for (let index = 1; index < candidates.length; index += 1) {
    const candidate = candidates[index];
    if (!candidate) {
      continue;
    }
    const store = loadSessionStore(candidate.storePath, loadOptions);
    const match = findFreshestStoreMatch(store, ...scanTargets);
    if (!match) {
      continue;
    }
    const updatedAt = match.entry.updatedAt ?? 0;
    // Mirror combined-store merge behavior so follow-up mutations target the
    // same backing store that won the listing merge when ids collide.
    if (!selectedMatch || updatedAt >= selectedUpdatedAt) {
      selectedStorePath = candidate.storePath;
      selectedStore = store;
      selectedMatch = match;
      selectedUpdatedAt = updatedAt;
    }
  }

  return {
    storePath: selectedStorePath,
    store: selectedStore,
    match: selectedMatch,
  };
}

function resolveExplicitDeletedLegacyMainStoreTarget(params: {
  cfg: OpenClawConfig;
  key: string;
  clone?: boolean;
  scanLegacyKeys?: boolean;
}): {
  agentId: string;
  storePath: string;
  canonicalKey: string;
  storeKeys: string[];
} | null {
  const parsed = parseAgentSessionKey(params.key);
  const legacyAgentId = normalizeAgentId(parsed?.agentId);
  if (
    !parsed ||
    legacyAgentId !== DEFAULT_AGENT_ID ||
    listAgentIds(params.cfg).includes(legacyAgentId)
  ) {
    return null;
  }

  // Only preserve agent:main:* when it is backed by a discovered deleted-main store.
  // Shared-store legacy aliases should continue remapping to the configured default agent.
  const canonicalKey = resolveStoredSessionKeyForAgentStore({
    cfg: params.cfg,
    agentId: legacyAgentId,
    sessionKey: params.key,
  });
  const agentMainKey = resolveAgentMainSessionKey({ cfg: params.cfg, agentId: legacyAgentId });
  const legacyAgentMainKey = `agent:${legacyAgentId}:main`;
  const lookupSeeds = Array.from(
    new Set([params.key, canonicalKey, agentMainKey, legacyAgentMainKey]),
  );
  let best:
    | {
        storePath: string;
        store: Record<string, SessionEntry>;
        match: { entry: SessionEntry; key: string };
      }
    | undefined;
  const loadOptions = params.clone === false ? { clone: false } : undefined;
  for (const target of resolveAllAgentSessionStoreTargetsSync(params.cfg)) {
    if (target.agentId !== legacyAgentId) {
      continue;
    }
    const store = loadSessionStore(target.storePath, loadOptions);
    const match = findFreshestStoreMatch(store, ...lookupSeeds);
    if (!match) {
      continue;
    }
    if (!best || (match.entry.updatedAt ?? 0) >= (best.match.entry.updatedAt ?? 0)) {
      best = { storePath: target.storePath, store, match };
    }
  }
  if (!best) {
    return null;
  }

  const storeKeys = new Set<string>([canonicalKey]);
  if (params.key !== canonicalKey) {
    storeKeys.add(params.key);
  }
  storeKeys.add(best.match.key);
  if (params.scanLegacyKeys !== false) {
    for (const seed of lookupSeeds) {
      storeKeys.add(seed);
      for (const legacyKey of findStoreKeysIgnoreCase(best.store, seed)) {
        storeKeys.add(legacyKey);
      }
    }
  }
  return {
    agentId: legacyAgentId,
    storePath: best.storePath,
    canonicalKey,
    storeKeys: Array.from(storeKeys),
  };
}

export function resolveGatewaySessionStoreTarget(params: {
  cfg: OpenClawConfig;
  key: string;
  agentId?: string;
  clone?: boolean;
  scanLegacyKeys?: boolean;
  store?: Record<string, SessionEntry>;
}): {
  agentId: string;
  storePath: string;
  canonicalKey: string;
  storeKeys: string[];
} {
  const key = normalizeOptionalString(params.key) ?? "";
  const explicitDeletedMainTarget = resolveExplicitDeletedLegacyMainStoreTarget({
    cfg: params.cfg,
    key,
    clone: params.clone,
    scanLegacyKeys: params.scanLegacyKeys,
  });
  if (explicitDeletedMainTarget) {
    return explicitDeletedMainTarget;
  }

  const canonicalKey = resolveSessionStoreKey({
    cfg: params.cfg,
    sessionKey: key,
  });
  const requestedAgentId = normalizeOptionalString(params.agentId);
  const agentId =
    canonicalKey === "global" && requestedAgentId
      ? normalizeAgentId(requestedAgentId)
      : resolveSessionStoreAgentId(params.cfg, canonicalKey);
  const { storePath, store } = resolveGatewaySessionStoreLookup({
    cfg: params.cfg,
    key,
    canonicalKey,
    agentId,
    clone: params.clone,
    initialStore: params.store,
  });

  if (canonicalKey === "global" || canonicalKey === "unknown") {
    const storeKeys = key && key !== canonicalKey ? [canonicalKey, key] : [key];
    return { agentId, storePath, canonicalKey, storeKeys };
  }

  const storeKeys = new Set<string>();
  storeKeys.add(canonicalKey);
  if (key && key !== canonicalKey) {
    storeKeys.add(key);
  }
  if (params.scanLegacyKeys !== false) {
    // Scan the on-disk store for case variants of every target to find
    // legacy mixed-case entries (e.g. "agent:ops:MAIN" when canonical is "agent:ops:work").
    const scanTargets = buildGatewaySessionStoreScanTargets({
      cfg: params.cfg,
      key,
      canonicalKey,
      agentId,
    });
    for (const seed of scanTargets) {
      for (const legacyKey of findStoreKeysIgnoreCase(store, seed)) {
        storeKeys.add(legacyKey);
      }
    }
  }
  return {
    agentId,
    storePath,
    canonicalKey,
    storeKeys: Array.from(storeKeys),
  };
}

export { loadCombinedSessionStoreForGateway } from "../config/sessions/combined-store-gateway.js";

export function resolveGatewaySessionThinkingDefault(params: {
  cfg: OpenClawConfig;
  provider: string;
  model: string;
  agentId?: string;
  modelCatalog?: ModelCatalogEntry[];
}) {
  const agentThinkingDefault = params.agentId
    ? resolveAgentConfig(params.cfg, params.agentId)?.thinkingDefault
    : undefined;
  return (
    agentThinkingDefault ??
    resolveThinkingDefault({
      cfg: params.cfg,
      provider: params.provider,
      model: params.model,
      catalog: params.modelCatalog,
    })
  );
}

export function getSessionDefaults(
  cfg: OpenClawConfig,
  modelCatalog?: ModelCatalogEntry[],
  options?: { allowPluginNormalization?: boolean },
): GatewaySessionsDefaults {
  const resolved = resolveConfiguredModelRef({
    cfg,
    defaultProvider: DEFAULT_PROVIDER,
    defaultModel: DEFAULT_MODEL,
    allowPluginNormalization: options?.allowPluginNormalization,
  });
  const contextTokens =
    cfg.agents?.defaults?.contextTokens ??
    lookupContextTokens(resolved.model, { allowAsyncLoad: false }) ??
    DEFAULT_CONTEXT_TOKENS;
  const thinkingLevels = listThinkingLevelOptions(resolved.provider, resolved.model, modelCatalog);
  return {
    modelProvider: resolved.provider ?? null,
    model: resolved.model ?? null,
    contextTokens: contextTokens ?? null,
    thinkingLevels,
    thinkingOptions: thinkingLevels.map((level) => level.label),
    thinkingDefault: resolveGatewaySessionThinkingDefault({
      cfg,
      provider: resolved.provider,
      model: resolved.model,
      modelCatalog,
    }),
  };
}

export function resolveSessionModelRef(
  cfg: OpenClawConfig,
  entry?:
    | SessionEntry
    | Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">,
  agentId?: string,
  options?: { allowPluginNormalization?: boolean },
): { provider: string; model: string } {
  const normalizedOverride = normalizeStoredOverrideModel({
    providerOverride: entry?.providerOverride,
    modelOverride: entry?.modelOverride,
  });
  if (normalizedOverride.providerOverride && normalizedOverride.modelOverride) {
    return resolvePersistedSelectedModelRef({
      defaultProvider: normalizedOverride.providerOverride,
      overrideProvider: normalizedOverride.providerOverride,
      overrideModel: normalizedOverride.modelOverride,
      allowPluginNormalization: options?.allowPluginNormalization,
    })!;
  }
  const runtimeProvider = normalizeOptionalString(entry?.modelProvider);
  const runtimeModel = normalizeOptionalString(entry?.model);
  if (runtimeProvider && runtimeModel) {
    return { provider: runtimeProvider, model: runtimeModel };
  }

  const resolved = agentId
    ? resolveDefaultModelForAgent({
        cfg,
        agentId,
        allowPluginNormalization: options?.allowPluginNormalization,
      })
    : resolveConfiguredModelRef({
        cfg,
        defaultProvider: DEFAULT_PROVIDER,
        defaultModel: DEFAULT_MODEL,
        allowPluginNormalization: options?.allowPluginNormalization,
      });

  const persisted = resolvePersistedSelectedModelRef({
    defaultProvider: resolved.provider || DEFAULT_PROVIDER,
    runtimeProvider,
    runtimeModel,
    overrideProvider: normalizedOverride.providerOverride,
    overrideModel: normalizedOverride.modelOverride,
    allowPluginNormalization: options?.allowPluginNormalization,
  });
  if (persisted) {
    return persisted;
  }
  return resolved;
}

export async function resolveGatewayModelSupportsImages(params: {
  loadGatewayModelCatalog: (params?: { readOnly?: boolean }) => Promise<ModelCatalogEntry[]>;
  provider?: string;
  model?: string;
}): Promise<boolean> {
  if (!params.model) {
    return true;
  }

  try {
    const catalog = await params.loadGatewayModelCatalog({ readOnly: false });
    const modelEntry = findModelCatalogEntry(catalog, {
      provider: params.provider,
      modelId: params.model,
    });
    const normalizedProvider = normalizeOptionalLowercaseString(
      params.provider ?? modelEntry?.provider,
    );
    const normalizedCandidates = [
      normalizeLowercaseStringOrEmpty(params.model),
      normalizeLowercaseStringOrEmpty(modelEntry?.name),
    ].filter(Boolean);
    if (modelEntry) {
      if (modelSupportsInput(modelEntry, "image")) {
        return true;
      }
      // Legacy safety shim for stale persisted Foundry rows that predate
      // provider-owned capability normalization.
      if (
        normalizedProvider === "microsoft-foundry" &&
        normalizedCandidates.some(
          (candidate) =>
            candidate.startsWith("gpt-") ||
            candidate.startsWith("o1") ||
            candidate.startsWith("o3") ||
            candidate.startsWith("o4") ||
            candidate === "computer-use-preview",
        )
      ) {
        return true;
      }
      if (
        normalizedProvider === "claude-cli" &&
        normalizedCandidates.some(
          (candidate) =>
            candidate === "opus" ||
            candidate === "sonnet" ||
            candidate === "haiku" ||
            candidate.startsWith("claude-"),
        )
      ) {
        return true;
      }
      return false;
    }
    if (
      normalizedProvider === "claude-cli" &&
      normalizedCandidates.some(
        (candidate) =>
          candidate === "opus" ||
          candidate === "sonnet" ||
          candidate === "haiku" ||
          candidate.startsWith("claude-"),
      )
    ) {
      return true;
    }
    return false;
  } catch {
    return false;
  }
}

export function resolveSessionModelIdentityRef(
  cfg: OpenClawConfig,
  entry?:
    | SessionEntry
    | Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">,
  agentId?: string,
  fallbackModelRef?: string,
  options?: { allowPluginNormalization?: boolean },
): { provider?: string; model: string } {
  const runtimeModel = entry?.model?.trim();
  const runtimeProvider = entry?.modelProvider?.trim();
  if (runtimeModel) {
    if (runtimeProvider) {
      return { provider: runtimeProvider, model: runtimeModel };
    }
    const inferredProvider = inferUniqueProviderFromConfiguredModels({
      cfg,
      model: runtimeModel,
    });
    if (inferredProvider) {
      return { provider: inferredProvider, model: runtimeModel };
    }
    if (runtimeModel.includes("/")) {
      const parsedRuntime = parseModelRef(runtimeModel, DEFAULT_PROVIDER, {
        allowPluginNormalization: options?.allowPluginNormalization,
      });
      if (parsedRuntime) {
        return { provider: parsedRuntime.provider, model: parsedRuntime.model };
      }
      return { model: runtimeModel };
    }
    return { model: runtimeModel };
  }
  const fallbackRef = fallbackModelRef?.trim();
  if (fallbackRef) {
    const parsedFallback = parseModelRef(fallbackRef, DEFAULT_PROVIDER, {
      allowPluginNormalization: options?.allowPluginNormalization,
    });
    if (parsedFallback) {
      return { provider: parsedFallback.provider, model: parsedFallback.model };
    }
    const inferredProvider = inferUniqueProviderFromConfiguredModels({
      cfg,
      model: fallbackRef,
    });
    if (inferredProvider) {
      return { provider: inferredProvider, model: fallbackRef };
    }
    return { model: fallbackRef };
  }
  const resolved = resolveSessionModelRef(cfg, entry, agentId, {
    allowPluginNormalization: options?.allowPluginNormalization,
  });
  return { provider: resolved.provider, model: resolved.model };
}

function resolveSessionDisplayModelIdentityRefCached(params: {
  cfg: OpenClawConfig;
  agentId: string;
  provider?: string;
  model?: string;
  rowContext?: SessionListRowContext;
}): { provider?: string; model?: string } {
  const ctx = params.rowContext;
  if (!ctx) {
    return resolveSessionDisplayModelIdentityRef(params);
  }
  const key = `${params.agentId}\u0000${createSessionRowModelCacheKey(
    params.provider,
    params.model,
  )}`;
  const cached = ctx.displayModelIdentityByKey.get(key);
  if (cached) {
    return cached;
  }
  const value = resolveSessionDisplayModelIdentityRef(params);
  ctx.displayModelIdentityByKey.set(key, value);
  return value;
}

export function resolveSessionDisplayModelIdentityRef(params: {
  cfg: OpenClawConfig;
  agentId: string;
  provider?: string;
  model?: string;
}): { provider?: string; model?: string } {
  const provider = normalizeOptionalString(params.provider);
  const model = normalizeOptionalString(params.model);
  if (!provider || !model || !isCliProvider(provider, params.cfg)) {
    return { provider, model };
  }

  const defaultRef = resolveDefaultModelForAgent({ cfg: params.cfg, agentId: params.agentId });
  if (model.includes("/")) {
    const parsedModel = parseModelRef(model, defaultRef.provider);
    if (parsedModel && !isCliProvider(parsedModel.provider, params.cfg)) {
      return parsedModel;
    }
  }

  const inferredProvider = inferUniqueProviderFromConfiguredModels({
    cfg: params.cfg,
    model,
  });
  if (inferredProvider && !isCliProvider(inferredProvider, params.cfg)) {
    return { provider: inferredProvider, model };
  }

  const parsedModel = parseModelRef(model, defaultRef.provider);
  if (parsedModel && !isCliProvider(parsedModel.provider, params.cfg)) {
    return parsedModel;
  }

  return {
    provider: defaultRef.provider || provider,
    model,
  };
}

export function buildGatewaySessionRow(params: {
  cfg: OpenClawConfig;
  storePath: string;
  store: Record<string, SessionEntry>;
  key: string;
  entry?: SessionEntry;
  modelCatalog?: ModelCatalogEntry[];
  now?: number;
  includeDerivedTitles?: boolean;
  includeLastMessage?: boolean;
  transcriptUsageMaxBytes?: number;
  storeChildSessionsByKey?: Map<string, string[]>;
  rowContext?: SessionListRowContext;
  agentId?: string;
  skipTranscriptUsageFallback?: boolean;
  lightweightListRow?: boolean;
}): GatewaySessionRow {
  const { cfg, storePath, store, key, entry } = params;
  const lightweight = params.lightweightListRow === true;
  const skipTranscriptUsage = params.skipTranscriptUsageFallback === true;
  const now = params.now ?? Date.now();
  const updatedAt = entry?.updatedAt ?? null;
  const parsed = parseGroupKey(key);
  const channel = entry?.channel ?? parsed?.channel;
  const subject = entry?.subject;
  const groupChannel = entry?.groupChannel;
  const space = entry?.space;
  const id = parsed?.id;
  const origin = entry?.origin;
  const originLabel = origin?.label;
  const displayName =
    entry?.displayName ??
    (channel
      ? buildGroupDisplayName({
          provider: channel,
          subject,
          groupChannel,
          space,
          id,
          key,
        })
      : undefined) ??
    entry?.label ??
    originLabel;
  const deliveryFields = normalizeSessionDeliveryFields(entry);
  const parsedAgent = parseAgentSessionKey(key);
  const sessionAgentId = normalizeAgentId(
    parsedAgent?.agentId ?? params.agentId ?? resolveDefaultAgentId(cfg),
  );
  const rowContext = params.rowContext;
  const subagentRun = rowContext
    ? rowContext.subagentRuns.getDisplaySubagentRun(key)
    : getSessionDisplaySubagentRunByChildSessionKey(key);
  const subagentOwner =
    normalizeOptionalString(subagentRun?.controllerSessionKey) ||
    normalizeOptionalString(subagentRun?.requesterSessionKey);
  const liveSubagentRunActive = isSubagentRunLive(subagentRun);
  const persistedSessionStatus = entry?.status;
  const persistedSessionEndedAt = entry?.endedAt;
  const persistedSessionStartedAt = entry?.startedAt;
  const persistedSessionRuntimeMs = entry?.runtimeMs;
  const subagentRunState = subagentRun
    ? liveSubagentRunActive
      ? "active"
      : typeof subagentRun.endedAt === "number" ||
          persistedSessionStatus === "done" ||
          persistedSessionStatus === "failed" ||
          persistedSessionStatus === "killed" ||
          persistedSessionStatus === "timeout" ||
          typeof persistedSessionEndedAt === "number"
        ? "historical"
        : "interrupted"
    : undefined;
  const subagentStatus = subagentRun
    ? liveSubagentRunActive
      ? resolveSubagentSessionStatus(subagentRun)
      : persistedSessionStatus === "running"
        ? undefined
        : (persistedSessionStatus ??
          (typeof subagentRun.endedAt === "number"
            ? resolveSubagentSessionStatus(subagentRun)
            : undefined))
    : undefined;
  const subagentStartedAt = subagentRun
    ? liveSubagentRunActive
      ? getSubagentSessionStartedAt(subagentRun)
      : (persistedSessionStartedAt ?? getSubagentSessionStartedAt(subagentRun))
    : undefined;
  const subagentEndedAt = subagentRun
    ? liveSubagentRunActive
      ? subagentRun.endedAt
      : (persistedSessionEndedAt ?? subagentRun.endedAt)
    : undefined;
  const subagentRuntimeMs = subagentRun
    ? liveSubagentRunActive
      ? resolveSessionRuntimeMs(subagentRun, now)
      : (persistedSessionRuntimeMs ??
        (typeof subagentRun.endedAt === "number"
          ? resolveSessionRuntimeMs(subagentRun, now)
          : undefined))
    : undefined;
  const selectedModel = resolveSessionSelectedModelRef({
    cfg,
    entry,
    agentId: sessionAgentId,
    rowContext,
    allowPluginNormalization: !lightweight,
  });
  const resolvedModel = resolveSessionModelIdentityRef(
    cfg,
    entry,
    sessionAgentId,
    subagentRun?.model,
    { allowPluginNormalization: !lightweight },
  );
  const runtimeModelPresent =
    Boolean(entry?.model?.trim()) || Boolean(entry?.modelProvider?.trim());
  const needsTranscriptTotalTokens =
    resolvePositiveNumber(resolveFreshSessionTotalTokens(entry)) === undefined;
  const needsTranscriptContextTokens = resolvePositiveNumber(entry?.contextTokens) === undefined;
  const needsTranscriptEstimatedCostUsd =
    !skipTranscriptUsage &&
    resolveEstimatedSessionCostUsd({
      cfg,
      provider: resolvedModel.provider,
      model: resolvedModel.model ?? DEFAULT_MODEL,
      entry,
      rowContext,
    }) === undefined;
  const transcriptUsage =
    !skipTranscriptUsage &&
    (needsTranscriptTotalTokens || needsTranscriptContextTokens || needsTranscriptEstimatedCostUsd)
      ? resolveTranscriptUsageFallback({
          cfg,
          key,
          entry,
          storePath,
          fallbackProvider: resolvedModel.provider,
          fallbackModel: resolvedModel.model ?? DEFAULT_MODEL,
          maxTranscriptBytes: params.transcriptUsageMaxBytes,
          rowContext: params.rowContext,
          agentId: sessionAgentId,
        })
      : null;
  const preferLiveSubagentModelIdentity =
    Boolean(subagentRun?.model?.trim()) && subagentStatus === "running";
  const shouldUseTranscriptModelIdentity =
    runtimeModelPresent &&
    !preferLiveSubagentModelIdentity &&
    (needsTranscriptTotalTokens || needsTranscriptContextTokens);
  const resolvedModelIdentity = {
    provider: resolvedModel.provider,
    model: resolvedModel.model ?? DEFAULT_MODEL,
  };
  const modelIdentity = shouldUseTranscriptModelIdentity
    ? {
        provider: transcriptUsage?.modelProvider ?? resolvedModelIdentity.provider,
        model: transcriptUsage?.model ?? resolvedModelIdentity.model,
      }
    : resolvedModelIdentity;
  const { provider: modelProvider, model } = modelIdentity;
  const totalTokens =
    resolvePositiveNumber(resolveFreshSessionTotalTokens(entry)) ??
    resolvePositiveNumber(transcriptUsage?.totalTokens);
  const totalTokensFresh =
    typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0
      ? true
      : transcriptUsage?.totalTokensFresh === true;
  const goal = entry?.goal
    ? resolveSessionGoalDisplayState(
        {
          goal: entry.goal,
          totalTokens,
          totalTokensFresh,
        },
        now,
        // Session listing is read-only; stale goal baselines are adopted only
        // by goal commands/tools that can persist the first fresh snapshot.
        { adoptFreshBaseline: false },
      )
    : undefined;
  const childSessions = params.storeChildSessionsByKey
    ? mergeChildSessionKeys(
        resolveRuntimeChildSessionKeys(key, now, rowContext?.subagentRuns),
        params.storeChildSessionsByKey.get(key),
      )
    : resolveChildSessionKeys(key, store, now, rowContext?.subagentRuns);
  const compactionCheckpoints = resolveProjectableCompactionCheckpoints(entry);
  const compactionCheckpointCount = Array.isArray(entry?.compactionCheckpoints)
    ? compactionCheckpoints.length
    : undefined;
  const latestCompactionCheckpoint = buildCompactionCheckpointPreview(
    resolveLatestCompactionCheckpoint(compactionCheckpoints),
  );
  const selectedOrRuntimeModelProvider = selectedModel?.provider ?? modelProvider;
  const selectedOrRuntimeModel = selectedModel?.model ?? model;
  const rowModelIdentity = lightweight
    ? { provider: selectedOrRuntimeModelProvider, model: selectedOrRuntimeModel }
    : resolveSessionDisplayModelIdentityRefCached({
        cfg,
        agentId: sessionAgentId,
        provider: selectedOrRuntimeModelProvider,
        model: selectedOrRuntimeModel,
        rowContext: params.rowContext,
      });
  const rowModelProvider = rowModelIdentity.provider;
  const rowModel = rowModelIdentity.model;
  const agentRuntime = resolveModelAgentRuntimeMetadata({
    cfg,
    agentId: sessionAgentId,
    provider: rowModelProvider,
    model: rowModel,
    sessionKey: key,
    acpRuntime: entry?.acp != null,
    acpBackend: entry?.acp?.backend,
  });
  const estimatedCostUsd = lightweight
    ? resolveNonNegativeNumber(entry?.estimatedCostUsd)
    : (resolveEstimatedSessionCostUsd({
        cfg,
        provider: rowModelProvider,
        model: rowModel,
        entry,
        rowContext: params.rowContext,
      }) ?? resolveNonNegativeNumber(transcriptUsage?.estimatedCostUsd));
  const contextTokens = lightweight
    ? resolvePositiveNumber(entry?.contextTokens)
    : (resolvePositiveNumber(entry?.contextTokens) ??
      resolvePositiveNumber(transcriptUsage?.contextTokens) ??
      resolvePositiveNumber(
        resolveContextTokensForModel({
          cfg,
          provider: rowModelProvider,
          model: rowModel,
          allowAsyncLoad: false,
        }),
      ));

  let derivedTitle: string | undefined;
  let lastMessagePreview: string | undefined;
  if (entry?.sessionId && (params.includeDerivedTitles || params.includeLastMessage)) {
    const fields = readSessionTitleFieldsFromTranscript(
      entry.sessionId,
      storePath,
      entry.sessionFile,
      sessionAgentId,
    );
    if (params.includeDerivedTitles) {
      derivedTitle = deriveSessionTitle(entry, fields.firstUserMessage);
    }
    if (params.includeLastMessage && fields.lastMessagePreview) {
      lastMessagePreview = fields.lastMessagePreview;
    }
  }

  const thinkingProvider = rowModelProvider ?? DEFAULT_PROVIDER;
  const thinkingModel = rowModel ?? DEFAULT_MODEL;
  const thinkingMetadata = resolveSessionRowThinkingMetadata({
    cfg,
    agentId: sessionAgentId,
    provider: thinkingProvider,
    model: thinkingModel,
    modelCatalog: params.modelCatalog,
    rowContext,
  });
  const thinkingLevels = thinkingMetadata.levels;
  const thinkingDefault = thinkingMetadata.defaultLevel;
  const pluginExtensions =
    !lightweight && entry ? projectPluginSessionExtensionsSync({ sessionKey: key, entry }) : [];

  return {
    key,
    spawnedBy: subagentOwner || entry?.spawnedBy,
    spawnedWorkspaceDir: entry?.spawnedWorkspaceDir,
    spawnedCwd: entry?.spawnedCwd,
    forkedFromParent: entry?.forkedFromParent,
    spawnDepth: entry?.spawnDepth,
    subagentRole: entry?.subagentRole,
    subagentControlScope: entry?.subagentControlScope,
    kind: classifySessionKey(key, entry),
    label: entry?.label,
    displayName,
    derivedTitle,
    lastMessagePreview,
    channel,
    subject,
    groupChannel,
    space,
    chatType: entry?.chatType,
    origin,
    updatedAt,
    sessionId: entry?.sessionId,
    systemSent: entry?.systemSent,
    abortedLastRun: entry?.abortedLastRun,
    thinkingLevel: entry?.thinkingLevel,
    thinkingLevels,
    thinkingOptions: thinkingLevels.map((level) => level.label),
    thinkingDefault,
    fastMode: entry?.fastMode,
    verboseLevel: entry?.verboseLevel,
    traceLevel: entry?.traceLevel,
    reasoningLevel: entry?.reasoningLevel,
    elevatedLevel: entry?.elevatedLevel,
    sendPolicy: entry?.sendPolicy,
    inputTokens: entry?.inputTokens,
    outputTokens: entry?.outputTokens,
    totalTokens,
    totalTokensFresh,
    goal,
    estimatedCostUsd,
    status: subagentRun ? subagentStatus : entry?.status,
    subagentRunState,
    hasActiveSubagentRun: subagentRun ? liveSubagentRunActive : undefined,
    startedAt: subagentRun ? subagentStartedAt : entry?.startedAt,
    endedAt: subagentRun ? subagentEndedAt : entry?.endedAt,
    runtimeMs: subagentRun ? subagentRuntimeMs : entry?.runtimeMs,
    parentSessionKey: subagentOwner || entry?.parentSessionKey,
    childSessions,
    responseUsage: entry?.responseUsage,
    modelProvider: rowModelProvider,
    model: rowModel,
    agentRuntime,
    contextTokens,
    contextBudgetStatus: entry?.contextBudgetStatus,
    deliveryContext: deliveryFields.deliveryContext,
    lastChannel: deliveryFields.lastChannel ?? entry?.lastChannel,
    lastTo: deliveryFields.lastTo ?? entry?.lastTo,
    lastAccountId: deliveryFields.lastAccountId ?? entry?.lastAccountId,
    lastThreadId: deliveryFields.lastThreadId ?? entry?.lastThreadId,
    compactionCheckpointCount,
    latestCompactionCheckpoint,
    pluginExtensions: pluginExtensions.length > 0 ? pluginExtensions : undefined,
  };
}

function resolveSessionListSearchDisplayName(
  key: string,
  entry?: SessionEntry,
): string | undefined {
  if (entry?.displayName) {
    return entry.displayName;
  }
  const parsed = parseGroupKey(key);
  const channel = entry?.channel ?? parsed?.channel;
  if (!channel) {
    return undefined;
  }
  return buildGroupDisplayName({
    provider: channel,
    subject: entry?.subject,
    groupChannel: entry?.groupChannel,
    space: entry?.space,
    id: parsed?.id,
    key,
  });
}

function addSessionListSearchModelFields(
  fields: Array<string | undefined>,
  identity: { provider?: string; model?: string },
) {
  const provider = normalizeOptionalString(identity.provider);
  const model = normalizeOptionalString(identity.model);
  fields.push(provider, model);
  if (provider && model) {
    fields.push(`${provider}/${model}`);
  }
}

function resolveSessionListSearchModelFields(params: {
  cfg: OpenClawConfig;
  key: string;
  entry?: SessionEntry;
  rowContext?: SessionListRowContext;
}): Array<string | undefined> {
  const parsedAgent = parseAgentSessionKey(params.key);
  const agentId = normalizeAgentId(parsedAgent?.agentId ?? resolveDefaultAgentId(params.cfg));
  const subagentRun = params.rowContext
    ? params.rowContext.subagentRuns.getDisplaySubagentRun(params.key)
    : getSessionDisplaySubagentRunByChildSessionKey(params.key);
  const selectedModel = resolveSessionSelectedModelRef({
    cfg: params.cfg,
    entry: params.entry,
    agentId,
    rowContext: params.rowContext,
    allowPluginNormalization: false,
  });
  const resolvedModel = resolveSessionModelIdentityRef(
    params.cfg,
    params.entry,
    agentId,
    subagentRun?.model,
    { allowPluginNormalization: false },
  );
  const modelIdentity = {
    provider: resolvedModel.provider,
    model: resolvedModel.model ?? DEFAULT_MODEL,
  };
  const selectedOrRuntimeModelProvider = selectedModel?.provider ?? modelIdentity.provider;
  const selectedOrRuntimeModel = selectedModel?.model ?? modelIdentity.model;
  const displayModelIdentity = resolveSessionDisplayModelIdentityRefCached({
    cfg: params.cfg,
    agentId,
    provider: selectedOrRuntimeModelProvider,
    model: selectedOrRuntimeModel,
    rowContext: params.rowContext,
  });
  const fields: Array<string | undefined> = [];
  addSessionListSearchModelFields(fields, {
    provider: params.entry?.modelProvider,
    model: params.entry?.model,
  });
  addSessionListSearchModelFields(fields, resolvedModel);
  if (selectedModel) {
    addSessionListSearchModelFields(fields, selectedModel);
  }
  addSessionListSearchModelFields(fields, displayModelIdentity);
  return fields;
}

export function loadGatewaySessionRow(
  sessionKey: string,
  options?: {
    agentId?: string;
    includeDerivedTitles?: boolean;
    includeLastMessage?: boolean;
    now?: number;
    transcriptUsageMaxBytes?: number;
  },
): GatewaySessionRow | null {
  const now = options?.now ?? Date.now();
  const { cfg, storePath, store, entry, canonicalKey } = loadSessionEntry(sessionKey, {
    clone: false,
    ...(options?.agentId ? { agentId: options.agentId } : {}),
  });
  if (!entry) {
    return null;
  }
  const storeChildSessionsByKey = buildSingleRowStoreChildSessionsByKey({
    storePath,
    store,
    key: canonicalKey,
    now,
  });
  return buildGatewaySessionRow({
    cfg,
    storePath,
    store,
    key: canonicalKey,
    entry,
    now,
    includeDerivedTitles: options?.includeDerivedTitles,
    includeLastMessage: options?.includeLastMessage,
    transcriptUsageMaxBytes: options?.transcriptUsageMaxBytes,
    storeChildSessionsByKey,
    ...(options?.agentId ? { agentId: options.agentId } : {}),
  });
}

/**
 * Number of session rows to build per batch before yielding to the event loop.
 * Keeps the main thread responsive during large session list operations while
 * avoiding excessive yielding overhead for small stores.
 */
const SESSIONS_LIST_YIELD_BATCH_SIZE = 10;
const SESSIONS_LIST_TOP_N_LIMIT = 200;
const SESSIONS_LIST_DEFAULT_LIMIT = 100;

type SessionEntryPair = [string, SessionEntry];
type SessionEntrySelection = {
  entries: SessionEntryPair[];
  totalCount: number;
  limitApplied?: number;
  offset: number;
  nextOffset: number | null;
  hasMore: boolean;
};

function compareSessionEntryPairsByUpdatedAt(a: SessionEntryPair, b: SessionEntryPair): number {
  return (b[1]?.updatedAt ?? 0) - (a[1]?.updatedAt ?? 0);
}

function resolveSessionsListLimit(
  opts: SessionsListParams,
  defaultLimit?: number,
): number | undefined {
  if (typeof opts.limit !== "number" || !Number.isFinite(opts.limit)) {
    return defaultLimit;
  }
  return Math.max(1, Math.floor(opts.limit));
}

function resolveSessionsListOffset(opts: SessionsListParams): number {
  if (typeof opts.offset !== "number" || !Number.isFinite(opts.offset)) {
    return 0;
  }
  return Math.max(0, Math.floor(opts.offset));
}

function resolveSessionsListWindowLimit(limit: number | undefined, offset: number) {
  if (limit === undefined) {
    return undefined;
  }
  const windowLimit = offset + limit;
  return Number.isFinite(windowLimit) ? Math.min(windowLimit, Number.MAX_SAFE_INTEGER) : undefined;
}

function selectNewestLimitedEntries(
  entries: SessionEntryPair[],
  limit: number,
): SessionEntryPair[] {
  const selected: SessionEntryPair[] = [];
  for (const entry of entries) {
    const insertAt = selected.findIndex(
      (candidate) => compareSessionEntryPairsByUpdatedAt(entry, candidate) < 0,
    );
    if (insertAt >= 0) {
      selected.splice(insertAt, 0, entry);
      if (selected.length > limit) {
        selected.pop();
      }
    } else if (selected.length < limit) {
      selected.push(entry);
    }
  }
  return selected;
}

function sortAndLimitSessionEntries(
  entries: SessionEntryPair[],
  limit: number | undefined,
): SessionEntryPair[] {
  if (limit !== undefined && limit <= SESSIONS_LIST_TOP_N_LIMIT) {
    return selectNewestLimitedEntries(entries, limit);
  }
  const sorted = entries.toSorted(compareSessionEntryPairsByUpdatedAt);
  return limit === undefined ? sorted : sorted.slice(0, limit);
}

function filterSessionEntries(params: {
  cfg: OpenClawConfig;
  store: Record<string, SessionEntry>;
  opts: SessionsListParams;
  now: number;
  rowContext?: SessionListRowContext;
}): SessionEntryPair[] {
  const { cfg, store, opts, now } = params;
  const rowContext = params.rowContext;
  const includeGlobal = opts.includeGlobal === true;
  const includeUnknown = opts.includeUnknown === true;
  const spawnedBy = typeof opts.spawnedBy === "string" ? opts.spawnedBy : "";
  const label = normalizeOptionalString(opts.label) ?? "";
  const agentId = typeof opts.agentId === "string" ? normalizeAgentId(opts.agentId) : "";
  const search = normalizeLowercaseStringOrEmpty(opts.search);
  const activeMinutes =
    typeof opts.activeMinutes === "number" && Number.isFinite(opts.activeMinutes)
      ? Math.max(1, Math.floor(opts.activeMinutes))
      : undefined;

  let entries = Object.entries(store)
    .filter(([key]) => {
      if (isCronRunSessionKey(key)) {
        return false;
      }
      if (!includeGlobal && key === "global") {
        return false;
      }
      if (!includeUnknown && key === "unknown") {
        return false;
      }
      if (agentId) {
        if (key === "global") {
          return includeGlobal;
        }
        if (key === "unknown") {
          return false;
        }
        const parsed = parseAgentSessionKey(key);
        if (!parsed) {
          return false;
        }
        return normalizeAgentId(parsed.agentId) === agentId;
      }
      return true;
    })
    .filter(([key, entry]) => {
      if (!spawnedBy) {
        return true;
      }
      if (key === "unknown" || key === "global") {
        return false;
      }
      const latest = rowContext
        ? rowContext.subagentRuns.getDisplaySubagentRun(key)
        : getSessionDisplaySubagentRunByChildSessionKey(key);
      if (latest) {
        const latestControllerSessionKey =
          normalizeOptionalString(latest.controllerSessionKey) ||
          normalizeOptionalString(latest.requesterSessionKey);
        return (
          latestControllerSessionKey === spawnedBy &&
          shouldKeepSubagentRunChildLink(latest, {
            activeDescendants: rowContext
              ? rowContext.subagentRuns.countActiveDescendantRuns(key)
              : countActiveDescendantRuns(key),
            now,
          })
        );
      }
      return (
        shouldKeepStoreOnlyChildLink(entry, now) &&
        (entry?.spawnedBy === spawnedBy || entry?.parentSessionKey === spawnedBy)
      );
    })
    .filter(([, entry]) => {
      if (!label) {
        return true;
      }
      return entry?.label === label;
    });

  if (search) {
    entries = entries.filter(([key, entry]) => {
      const fields = [
        resolveSessionListSearchDisplayName(key, entry),
        entry?.label,
        entry?.subject,
        entry?.sessionId,
        key,
        ...resolveSessionListSearchModelFields({
          cfg,
          key,
          entry,
          rowContext,
        }),
      ];
      return fields.some(
        (f) => typeof f === "string" && normalizeLowercaseStringOrEmpty(f).includes(search),
      );
    });
  }

  if (activeMinutes !== undefined) {
    const cutoff = now - activeMinutes * 60_000;
    entries = entries.filter(([, entry]) => (entry?.updatedAt ?? 0) >= cutoff);
  }

  return entries;
}

function selectSessionEntries(params: {
  cfg: OpenClawConfig;
  store: Record<string, SessionEntry>;
  opts: SessionsListParams;
  now: number;
  rowContext?: SessionListRowContext;
  defaultLimit?: number;
}): SessionEntrySelection {
  const filtered = filterSessionEntries(params);
  const limit = resolveSessionsListLimit(params.opts, params.defaultLimit);
  const offset = resolveSessionsListOffset(params.opts);
  const windowLimit = resolveSessionsListWindowLimit(limit, offset);
  const sortedWindow = sortAndLimitSessionEntries(filtered, windowLimit);
  const entries =
    limit === undefined ? sortedWindow.slice(offset) : sortedWindow.slice(offset, offset + limit);
  const nextOffset = offset + entries.length;
  const hasMore = nextOffset < filtered.length;
  return {
    entries,
    totalCount: filtered.length,
    limitApplied: limit,
    offset,
    nextOffset: hasMore ? nextOffset : null,
    hasMore,
  };
}

export function filterAndSortSessionEntries(params: {
  cfg: OpenClawConfig;
  store: Record<string, SessionEntry>;
  opts: SessionsListParams;
  now: number;
  rowContext?: SessionListRowContext;
}): [string, SessionEntry][] {
  return selectSessionEntries(params).entries;
}

export function listSessionsFromStore(params: {
  cfg: OpenClawConfig;
  storePath: string;
  store: Record<string, SessionEntry>;
  modelCatalog?: ModelCatalogEntry[];
  opts: SessionsListParams;
}): SessionsListResult {
  const { cfg, storePath, store, opts } = params;
  const now = Date.now();
  const sessionListTranscriptUsageMaxBytes = 64 * 1024;
  const sessionListTranscriptFieldRows = 100;
  let rowContext: SessionListRowContext | undefined;
  const getRowContext = () => {
    rowContext ??= buildSessionListRowContext({ store, now });
    return rowContext;
  };
  const includeDerivedTitles = opts.includeDerivedTitles === true;
  const includeLastMessage = opts.includeLastMessage === true;
  const hasSpawnedByFilter = typeof opts.spawnedBy === "string" && opts.spawnedBy.length > 0;

  const selection = selectSessionEntries({
    cfg,
    store,
    opts,
    now,
    rowContext:
      hasSpawnedByFilter || Boolean(normalizeOptionalString(opts.search))
        ? getRowContext()
        : undefined,
    defaultLimit: SESSIONS_LIST_DEFAULT_LIMIT,
  });
  const { entries, totalCount, limitApplied, offset, nextOffset, hasMore } = selection;

  const sessions = entries.map(([key, entry], index) => {
    const includeTranscriptFields = index < sessionListTranscriptFieldRows;
    const rowAgentId =
      key === "global" && typeof opts.agentId === "string"
        ? normalizeAgentId(opts.agentId)
        : undefined;
    return buildGatewaySessionRow({
      cfg,
      storePath,
      store,
      key,
      entry,
      agentId: rowAgentId,
      modelCatalog: params.modelCatalog,
      now,
      includeDerivedTitles: includeTranscriptFields && includeDerivedTitles,
      includeLastMessage: includeTranscriptFields && includeLastMessage,
      transcriptUsageMaxBytes: sessionListTranscriptUsageMaxBytes,
      storeChildSessionsByKey: getRowContext().storeChildSessionsByKey,
      rowContext: getRowContext(),
    });
  });

  return {
    ts: now,
    path: storePath,
    count: sessions.length,
    totalCount,
    limitApplied,
    offset: offset > 0 ? offset : undefined,
    nextOffset,
    hasMore,
    defaults: getSessionDefaults(cfg, params.modelCatalog, { allowPluginNormalization: false }),
    sessions,
  };
}

/**
 * Async version of listSessionsFromStore that yields to the event loop between
 * batches of session row builds. This prevents large session stores from
 * blocking the event loop during sessions.list requests.
 *
 * The synchronous file I/O in readSessionTitleFieldsFromTranscript (head/tail
 * reads for derived titles and last-message previews) is the dominant blocker.
 * By yielding every SESSIONS_LIST_YIELD_BATCH_SIZE rows, we keep the event
 * loop responsive for WebSocket heartbeats, channel I/O, and concurrent RPC.
 */
export async function listSessionsFromStoreAsync(params: {
  cfg: OpenClawConfig;
  storePath: string;
  store: Record<string, SessionEntry>;
  modelCatalog?: ModelCatalogEntry[];
  opts: SessionsListParams;
}): Promise<SessionsListResult> {
  const { cfg, storePath, store, opts } = params;
  const now = Date.now();
  const sessionListTranscriptUsageMaxBytes = 64 * 1024;
  const sessionListTranscriptFieldRows = 100;
  let rowContext: SessionListRowContext | undefined;
  const getRowContext = () => {
    rowContext ??= buildSessionListRowContext({ store, now });
    return rowContext;
  };
  const includeDerivedTitles = opts.includeDerivedTitles === true;
  const includeLastMessage = opts.includeLastMessage === true;
  const hasSpawnedByFilter = typeof opts.spawnedBy === "string" && opts.spawnedBy.length > 0;

  const selection = selectSessionEntries({
    cfg,
    store,
    opts,
    now,
    rowContext:
      hasSpawnedByFilter || Boolean(normalizeOptionalString(opts.search))
        ? getRowContext()
        : undefined,
    defaultLimit: SESSIONS_LIST_DEFAULT_LIMIT,
  });
  const { entries, totalCount, limitApplied, offset, nextOffset, hasMore } = selection;

  const sessions: GatewaySessionRow[] = [];
  for (let i = 0; i < entries.length; i++) {
    const [key, entry] = entries[i];
    const includeTranscriptFields = i < sessionListTranscriptFieldRows;
    const rowAgentId =
      key === "global" && typeof opts.agentId === "string"
        ? normalizeAgentId(opts.agentId)
        : undefined;
    const row = buildGatewaySessionRow({
      cfg,
      storePath,
      store,
      key,
      entry,
      agentId: rowAgentId,
      modelCatalog: params.modelCatalog,
      now,
      includeDerivedTitles: false,
      includeLastMessage: false,
      transcriptUsageMaxBytes: sessionListTranscriptUsageMaxBytes,
      storeChildSessionsByKey: getRowContext().storeChildSessionsByKey,
      rowContext: getRowContext(),
      skipTranscriptUsageFallback: true,
      lightweightListRow: true,
    });
    if (
      entry?.sessionId &&
      includeTranscriptFields &&
      (includeDerivedTitles || includeLastMessage)
    ) {
      const parsed = parseAgentSessionKey(key);
      const sessionAgentId =
        rowAgentId ??
        (parsed?.agentId ? normalizeAgentId(parsed.agentId) : resolveDefaultAgentId(cfg));
      const fields = await readSessionTitleFieldsFromTranscriptAsync(
        entry.sessionId,
        storePath,
        entry.sessionFile,
        sessionAgentId,
      );
      if (includeDerivedTitles) {
        row.derivedTitle = deriveSessionTitle(entry, fields.firstUserMessage);
      }
      if (includeLastMessage && fields.lastMessagePreview) {
        row.lastMessagePreview = fields.lastMessagePreview;
      }
    }
    sessions.push(row);
    // Yield to the event loop between batches so WebSocket heartbeats,
    // channel I/O, and concurrent RPC calls are not starved.
    if ((i + 1) % SESSIONS_LIST_YIELD_BATCH_SIZE === 0 && i + 1 < entries.length) {
      await new Promise<void>((resolve) => setImmediate(resolve));
    }
  }

  return {
    ts: now,
    path: storePath,
    count: sessions.length,
    totalCount,
    limitApplied,
    offset: offset > 0 ? offset : undefined,
    nextOffset,
    hasMore,
    defaults: getSessionDefaults(cfg, params.modelCatalog, { allowPluginNormalization: false }),
    sessions,
  };
}
