UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

1,665 lines • 66.5 kB
import { render } from 'ink';
import React from 'react';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { l as logger, h as connectionState, A as ApiClient, r as readSettings, p as projectPath, n as usageFromAgentTokenCount } from './types-vIstUf63.mjs';
import { c as createSessionMetadata } from './createSessionMetadata-DCBEuGVc.mjs';
import { k as initialMachineMetadata, j as ensureActiveOrg, x as notifyDaemonSessionStarted, m as MessageQueue2, o as hashObject, s as MessageBuffer, H as ARTIFACT_SYSTEM_STEER, I as ATLAS_SYSTEM_STEER, l as registerKillSessionHandler, G as startConsortiumServer, N as provisionAcpSession, F as stopCaffeinate } from './index-Ucc63AlK.mjs';
import { s as setupOfflineReconnection } from './setupOfflineReconnection-C6XymNDI.mjs';
import { A as AcpBackend } from './capabilities-Cq3eleV5.mjs';
import { r as readGeminiLocalConfig, G as GEMINI_API_KEY_ENV, a as GOOGLE_API_KEY_ENV, d as determineGeminiModel, g as getGeminiModelSource, b as GEMINI_MODEL_ENV, c as getInitialGeminiModel } from './config-DUil2nTO.mjs';
import { c as createRunnerAttachments, G as GeminiDisplay } from './GeminiDisplay-Dh1Ms5gG.mjs';
import { B as BasePermissionHandler } from './BasePermissionHandler-CrSEra7L.mjs';
import { B as BaseReasoningProcessor } from './BaseReasoningProcessor-_ULxhoD7.mjs';
import { r as resolveModelFromMeta } from './resolveModelFromMeta-X99NL72i.mjs';
import { p as parseOptionsFromText, f as formatOptionsXml, h as hasIncompleteOptions } from './optionsParser-2TzGtgao.mjs';
import 'axios';
import 'chalk';
import 'fs';
import 'node:fs';
import 'node:os';
import 'node:events';
import 'socket.io-client';
import 'zod';
import 'tweetnacl';
import 'node:module';
import 'child_process';
import 'util';
import 'fs/promises';
import 'os';
import 'crypto';
import 'path';
import 'url';
import 'node:child_process';
import 'node:fs/promises';
import 'node:util';
import 'http';
import 'net';
import 'expo-server-sdk';
import 'node:net';
import 'node:readline';
import 'node:url';
import 'node:http';
import 'node:https';
import 'node:stream/promises';
import 'tar';
import 'ps-list';
import 'cross-spawn';
import 'tmp';
import 'qrcode-terminal';
import 'open';
import 'fastify';
import 'fastify-type-provider-zod';
import 'ws';
import 'playwright-core';
import 'imapflow';
import 'mailparser';
import 'yaml';
import '@modelcontextprotocol/sdk/client/index.js';
import '@modelcontextprotocol/sdk/client/streamableHttp.js';
import 'readline';
import '@modelcontextprotocol/sdk/server/mcp.js';
import '@modelcontextprotocol/sdk/server/streamableHttp.js';
import 'libsodium-wrappers';
import 'pixelmatch';
import 'pngjs';
import '@agentclientprotocol/sdk';

const GEMINI_TIMEOUTS = {
  /** Gemini CLI can be slow on first start (downloading models, etc.) */
  init: 12e4,
  /** Standard tool call timeout */
  toolCall: 12e4,
  /** Investigation tools (codebase_investigator) can run for a long time */
  investigation: 6e5,
  /** Think tools are usually quick */
  think: 3e4,
  /** Idle detection after last message chunk */
  idle: 500
};
const GEMINI_TOOL_PATTERNS = [
  {
    name: "change_title",
    patterns: ["change_title", "change-title", "consortium__change_title", "mcp__consortium__change_title"],
    inputFields: ["title"],
    emptyInputDefault: true
    // change_title often has empty input (title extracted from context)
  },
  {
    name: "save_memory",
    patterns: ["save_memory", "save-memory"],
    inputFields: ["memory", "content"]
  },
  {
    name: "think",
    patterns: ["think"],
    inputFields: ["thought", "thinking"]
  }
];
const AVAILABLE_MODELS = [
  "gemini-2.5-pro",
  "gemini-2.5-flash",
  "gemini-2.5-flash-lite"
];
class GeminiTransport {
  agentName = "gemini";
  /**
   * Gemini CLI needs 2 minutes for first start (model download, warm-up)
   */
  getInitTimeout() {
    return GEMINI_TIMEOUTS.init;
  }
  /**
   * Filter Gemini CLI debug output from stdout.
   *
   * Gemini CLI outputs various debug info (experiments, flags, etc.) to stdout
   * that breaks ACP JSON-RPC parsing. We only keep valid JSON lines.
   */
  filterStdoutLine(line) {
    const trimmed = line.trim();
    if (!trimmed) {
      return null;
    }
    if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
      return null;
    }
    try {
      const parsed = JSON.parse(trimmed);
      if (typeof parsed !== "object" || parsed === null) {
        return null;
      }
      return line;
    } catch {
      return null;
    }
  }
  /**
   * Handle Gemini CLI stderr output.
   *
   * Detects:
   * - Rate limit errors (429) - logged but not shown (CLI handles retries)
   * - Model not found (404) - emit error with available models
   * - Other errors during investigation - logged for debugging
   */
  handleStderr(text, context) {
    const trimmed = text.trim();
    if (!trimmed) {
      return { message: null, suppress: true };
    }
    if (trimmed.includes("status 429") || trimmed.includes('code":429') || trimmed.includes("rateLimitExceeded") || trimmed.includes("RESOURCE_EXHAUSTED")) {
      return {
        message: null,
        suppress: false
        // Log for debugging but don't show to user
      };
    }
    if (trimmed.includes("status 404") || trimmed.includes('code":404')) {
      const errorMessage = {
        type: "status",
        status: "error",
        detail: `Model not found. Available models: ${AVAILABLE_MODELS.join(", ")}`
      };
      return { message: errorMessage };
    }
    if (context.hasActiveInvestigation) {
      const hasError = trimmed.includes("timeout") || trimmed.includes("Timeout") || trimmed.includes("failed") || trimmed.includes("Failed") || trimmed.includes("error") || trimmed.includes("Error");
      if (hasError) {
        return { message: null, suppress: false };
      }
    }
    return { message: null };
  }
  /**
   * Gemini-specific tool patterns
   */
  getToolPatterns() {
    return GEMINI_TOOL_PATTERNS;
  }
  /**
   * Check if tool is an investigation tool (needs longer timeout)
   */
  isInvestigationTool(toolCallId, toolKind) {
    const lowerId = toolCallId.toLowerCase();
    return lowerId.includes("codebase_investigator") || lowerId.includes("investigator") || typeof toolKind === "string" && toolKind.includes("investigator");
  }
  /**
   * Get timeout for a tool call
   */
  getToolCallTimeout(toolCallId, toolKind) {
    if (this.isInvestigationTool(toolCallId, toolKind)) {
      return GEMINI_TIMEOUTS.investigation;
    }
    if (toolKind === "think") {
      return GEMINI_TIMEOUTS.think;
    }
    return GEMINI_TIMEOUTS.toolCall;
  }
  /**
   * Get idle detection timeout
   */
  getIdleTimeout() {
    return GEMINI_TIMEOUTS.idle;
  }
  /**
   * Extract tool name from toolCallId using Gemini patterns.
   *
   * Tool IDs often contain the tool name as a prefix (e.g., "change_title-1765385846663" -> "change_title")
   */
  extractToolNameFromId(toolCallId) {
    const lowerId = toolCallId.toLowerCase();
    for (const toolPattern of GEMINI_TOOL_PATTERNS) {
      for (const pattern of toolPattern.patterns) {
        if (lowerId.includes(pattern.toLowerCase())) {
          return toolPattern.name;
        }
      }
    }
    return null;
  }
  /**
   * Check if input is effectively empty
   */
  isEmptyInput(input) {
    if (!input) return true;
    if (Array.isArray(input)) return input.length === 0;
    if (typeof input === "object") return Object.keys(input).length === 0;
    return false;
  }
  /**
   * Determine the real tool name from various sources.
   *
   * When Gemini sends "other" or "Unknown tool", tries to determine the real name from:
   * 1. toolCallId patterns (most reliable - tool name often embedded in ID)
   * 2. Input field signatures (specific fields indicate specific tools)
   * 3. Empty input default (some tools like change_title have empty input)
   *
   * Context-based heuristics were removed as they were fragile and the above
   * methods cover all known cases.
   */
  determineToolName(toolName, toolCallId, input, _context) {
    if (toolName !== "other" && toolName !== "Unknown tool") {
      return toolName;
    }
    const idToolName = this.extractToolNameFromId(toolCallId);
    if (idToolName) {
      return idToolName;
    }
    if (input && typeof input === "object" && !Array.isArray(input)) {
      const inputKeys = Object.keys(input);
      for (const toolPattern of GEMINI_TOOL_PATTERNS) {
        if (toolPattern.inputFields) {
          const hasMatchingField = toolPattern.inputFields.some(
            (field) => inputKeys.some((key) => key.toLowerCase() === field.toLowerCase())
          );
          if (hasMatchingField) {
            return toolPattern.name;
          }
        }
      }
    }
    if (this.isEmptyInput(input) && toolName === "other") {
      const defaultTool = GEMINI_TOOL_PATTERNS.find((p) => p.emptyInputDefault);
      if (defaultTool) {
        return defaultTool.name;
      }
    }
    if (toolName === "other" || toolName === "Unknown tool") {
      const inputKeys = input && typeof input === "object" ? Object.keys(input) : [];
      logger.debug(
        `[GeminiTransport] Unknown tool pattern - toolCallId: "${toolCallId}", toolName: "${toolName}", inputKeys: [${inputKeys.join(", ")}]. Consider adding a new pattern to GEMINI_TOOL_PATTERNS if this tool appears frequently.`
      );
    }
    return toolName;
  }
}
const geminiTransport = new GeminiTransport();

function createGeminiBackend(options) {
  const localConfig = readGeminiLocalConfig();
  let apiKey = options.cloudToken || localConfig.token || process.env[GEMINI_API_KEY_ENV] || process.env[GOOGLE_API_KEY_ENV] || options.apiKey;
  if (!apiKey) {
    logger.warn(`[Gemini] No API key found. Run 'consortium connect gemini' to authenticate via Google OAuth, or set ${GEMINI_API_KEY_ENV} environment variable.`);
  }
  const geminiCommand = "gemini";
  const model = determineGeminiModel(options.model);
  const geminiArgs = ["--experimental-acp"];
  let googleCloudProject = null;
  if (localConfig.googleCloudProject) {
    const storedEmail = localConfig.googleCloudProjectEmail;
    const currentEmail = options.currentUserEmail;
    if (!storedEmail || storedEmail === currentEmail) {
      googleCloudProject = localConfig.googleCloudProject;
      logger.debug(`[Gemini] Using Google Cloud Project: ${googleCloudProject}${storedEmail ? ` (for ${storedEmail})` : " (global)"}`);
    } else {
      logger.debug(`[Gemini] Skipping stored Google Cloud Project (stored for ${storedEmail}, current user is ${currentEmail || "unknown"})`);
    }
  }
  const backendOptions = {
    agentName: "gemini",
    cwd: options.cwd,
    command: geminiCommand,
    args: geminiArgs,
    env: {
      ...options.env,
      ...apiKey ? { [GEMINI_API_KEY_ENV]: apiKey, [GOOGLE_API_KEY_ENV]: apiKey } : {},
      // Pass model via env var - gemini CLI reads GEMINI_MODEL automatically
      [GEMINI_MODEL_ENV]: model,
      // Pass Google Cloud Project for Workspace accounts
      ...googleCloudProject ? {
        GOOGLE_CLOUD_PROJECT: googleCloudProject,
        GOOGLE_CLOUD_PROJECT_ID: googleCloudProject
      } : {},
      // Suppress debug output from gemini CLI to avoid stdout pollution
      NODE_ENV: "production",
      DEBUG: ""
    },
    mcpServers: options.mcpServers,
    permissionHandler: options.permissionHandler,
    transportHandler: geminiTransport,
    // Check if prompt instructs the agent to change title (for auto-approval of change_title tool)
    hasChangeTitleInstruction: (prompt) => {
      const lower = prompt.toLowerCase();
      return lower.includes("change_title") || lower.includes("change title") || lower.includes("set title") || lower.includes("mcp__consortium__change_title");
    }
  };
  const modelSource = getGeminiModelSource(options.model);
  logger.debug("[Gemini] Creating ACP SDK backend with options:", {
    cwd: backendOptions.cwd,
    command: backendOptions.command,
    args: backendOptions.args,
    hasApiKey: !!apiKey,
    model,
    modelSource,
    mcpServerCount: options.mcpServers ? Object.keys(options.mcpServers).length : 0
  });
  return {
    backend: new AcpBackend(backendOptions),
    model,
    modelSource
  };
}

class GeminiPermissionHandler extends BasePermissionHandler {
  currentPermissionMode = "default";
  constructor(session) {
    super(session);
  }
  getLogPrefix() {
    return "[Gemini]";
  }
  /**
   * Update session reference (override for type visibility)
   */
  updateSession(newSession) {
    super.updateSession(newSession);
  }
  /**
   * Set the current permission mode
   * This affects how tool calls are automatically approved/denied
   */
  setPermissionMode(mode) {
    this.currentPermissionMode = mode;
    logger.debug(`${this.getLogPrefix()} Permission mode set to: ${mode}`);
  }
  /**
   * Check if a tool should be auto-approved based on permission mode
   */
  shouldAutoApprove(toolName, toolCallId, input) {
    const alwaysAutoApproveNames = ["change_title", "consortium__change_title", "GeminiReasoning", "CodexReasoning", "think", "save_memory"];
    const alwaysAutoApproveIds = ["change_title", "save_memory"];
    if (alwaysAutoApproveNames.some((name) => toolName.toLowerCase().includes(name.toLowerCase()))) {
      return true;
    }
    if (alwaysAutoApproveIds.some((id) => toolCallId.toLowerCase().includes(id.toLowerCase()))) {
      return true;
    }
    const writeTools = ["write", "edit", "create", "delete", "patch", "fs-edit", "bash", "shell", "exec", "terminal", "command", "kill"];
    const isWriteTool = writeTools.some((wt) => toolName.toLowerCase().includes(wt));
    switch (this.currentPermissionMode) {
      case "yolo":
        return true;
      case "safe-yolo":
        return !isWriteTool;
      case "read-only":
        return !isWriteTool;
      case "default":
      default:
        return false;
    }
  }
  /**
   * Handle a tool permission request
   * @param toolCallId - The unique ID of the tool call
   * @param toolName - The name of the tool being called
   * @param input - The input parameters for the tool
   * @returns Promise resolving to permission result
   */
  async handleToolCall(toolCallId, toolName, input) {
    if (this.shouldAutoApprove(toolName, toolCallId, input)) {
      logger.debug(`${this.getLogPrefix()} Auto-approving tool ${toolName} (${toolCallId}) in ${this.currentPermissionMode} mode`);
      this.session.updateAgentState((currentState) => ({
        ...currentState,
        completedRequests: {
          ...currentState.completedRequests,
          [toolCallId]: {
            tool: toolName,
            arguments: input,
            createdAt: Date.now(),
            completedAt: Date.now(),
            status: "approved",
            decision: this.currentPermissionMode === "yolo" ? "approved_for_session" : "approved"
          }
        }
      }));
      return {
        decision: this.currentPermissionMode === "yolo" ? "approved_for_session" : "approved"
      };
    }
    return this.createPendingRequest(toolCallId, toolName, input);
  }
}

class GeminiReasoningProcessor extends BaseReasoningProcessor {
  getToolName() {
    return "GeminiReasoning";
  }
  getLogPrefix() {
    return "[GeminiReasoningProcessor]";
  }
  /**
   * Process a reasoning chunk from agent_thought_chunk.
   * Gemini sends reasoning as chunks, we accumulate them similar to Codex.
   */
  processChunk(chunk) {
    this.processInput(chunk);
  }
  /**
   * Complete the reasoning section.
   * Called when reasoning is complete (e.g., when status changes to idle).
   * Returns true if reasoning was actually completed, false if there was nothing to complete.
   */
  complete() {
    return this.completeReasoning();
  }
}

class GeminiDiffProcessor {
  previousDiffs = /* @__PURE__ */ new Map();
  // Track diffs per file path
  onMessage = null;
  constructor(onMessage) {
    this.onMessage = onMessage || null;
  }
  /**
   * Process an fs-edit event and check if it contains diff information
   */
  processFsEdit(path, description, diff) {
    logger.debug(`[GeminiDiffProcessor] Processing fs-edit for path: ${path}`);
    if (diff) {
      this.processDiff(path, diff, description);
    } else {
      const simpleDiff = `File edited: ${path}${description ? ` - ${description}` : ""}`;
      this.processDiff(path, simpleDiff, description);
    }
  }
  /**
   * Process a tool result that may contain diff information
   */
  processToolResult(toolName, result, callId) {
    if (result && typeof result === "object") {
      const diff = result.diff || result.unified_diff || result.patch;
      const path = result.path || result.file;
      if (diff && path) {
        logger.debug(`[GeminiDiffProcessor] Found diff in tool result: ${toolName} (${callId})`);
        this.processDiff(path, diff, result.description);
      } else if (result.changes && typeof result.changes === "object") {
        for (const [filePath, change] of Object.entries(result.changes)) {
          const changeDiff = change.diff || change.unified_diff || JSON.stringify(change);
          this.processDiff(filePath, changeDiff, change.description);
        }
      }
    }
  }
  /**
   * Process a unified diff and check if it has changed from the previous value
   */
  processDiff(path, unifiedDiff, description) {
    const previousDiff = this.previousDiffs.get(path);
    if (previousDiff !== unifiedDiff) {
      logger.debug(`[GeminiDiffProcessor] Unified diff changed for ${path}, sending GeminiDiff tool call`);
      const callId = randomUUID();
      const toolCall = {
        type: "tool-call",
        name: "GeminiDiff",
        callId,
        input: {
          unified_diff: unifiedDiff,
          path,
          description
        },
        id: randomUUID()
      };
      this.onMessage?.(toolCall);
      const toolResult = {
        type: "tool-call-result",
        callId,
        output: {
          status: "completed"
        },
        id: randomUUID()
      };
      this.onMessage?.(toolResult);
    }
    this.previousDiffs.set(path, unifiedDiff);
    logger.debug(`[GeminiDiffProcessor] Updated stored diff for ${path}`);
  }
  /**
   * Reset the processor state (called on task_complete or turn_aborted)
   */
  reset() {
    logger.debug("[GeminiDiffProcessor] Resetting diff state");
    this.previousDiffs.clear();
  }
  /**
   * Set the message callback for sending messages directly
   */
  setMessageCallback(callback) {
    this.onMessage = callback;
  }
  /**
   * Get the current diff value for a specific path
   */
  getCurrentDiff(path) {
    return this.previousDiffs.get(path) || null;
  }
  /**
   * Get all tracked diffs
   */
  getAllDiffs() {
    return new Map(this.previousDiffs);
  }
}

class ConversationHistory {
  messages = [];
  maxMessages;
  maxCharacters;
  currentModel;
  constructor(options = {}) {
    this.maxMessages = options.maxMessages ?? 20;
    this.maxCharacters = options.maxCharacters ?? 5e4;
  }
  /**
   * Set the current model being used
   */
  setCurrentModel(model) {
    this.currentModel = model;
  }
  /**
   * Check if content is a duplicate of the last message with the same role.
   * Deduplication prevents inflating history when the same message is sent multiple times.
   */
  isDuplicate(role, content) {
    if (this.messages.length === 0) return false;
    for (let i = this.messages.length - 1; i >= 0; i--) {
      const msg = this.messages[i];
      if (msg.role === role) {
        const normalizedNew = content.trim().replace(/\s+/g, " ");
        const normalizedExisting = msg.content.replace(/\s+/g, " ");
        return normalizedNew === normalizedExisting;
      }
    }
    return false;
  }
  /**
   * Add a user message to history
   * Skips duplicate messages to prevent history inflation
   */
  addUserMessage(content) {
    if (!content.trim()) return;
    const trimmedContent = content.trim();
    if (this.isDuplicate("user", trimmedContent)) {
      logger.debug(`[ConversationHistory] Skipping duplicate user message (${trimmedContent.length} chars)`);
      return;
    }
    this.messages.push({
      role: "user",
      content: trimmedContent,
      timestamp: Date.now()
    });
    this.trimHistory();
    logger.debug(`[ConversationHistory] Added user message (${trimmedContent.length} chars), total: ${this.messages.length}`);
  }
  /**
   * Add an assistant response to history
   * Skips duplicate messages to prevent history inflation
   */
  addAssistantMessage(content) {
    if (!content.trim()) return;
    const trimmedContent = content.trim();
    if (this.isDuplicate("assistant", trimmedContent)) {
      logger.debug(`[ConversationHistory] Skipping duplicate assistant message (${trimmedContent.length} chars)`);
      return;
    }
    this.messages.push({
      role: "assistant",
      content: trimmedContent,
      timestamp: Date.now(),
      model: this.currentModel
    });
    this.trimHistory();
    logger.debug(`[ConversationHistory] Added assistant message (${trimmedContent.length} chars), total: ${this.messages.length}`);
  }
  /**
   * Get the number of messages in history
   */
  size() {
    return this.messages.length;
  }
  /**
   * Check if there's any history to preserve
   */
  hasHistory() {
    return this.messages.length > 0;
  }
  /**
   * Clear all history
   */
  clear() {
    this.messages = [];
    logger.debug("[ConversationHistory] History cleared");
  }
  /**
   * Get formatted context for injecting into a new session.
   * This is used when the model changes to preserve conversation context.
   * 
   * @returns Formatted string with previous conversation context, or empty string if no history
   */
  getContextForNewSession() {
    if (this.messages.length === 0) {
      return "";
    }
    const formattedMessages = this.messages.map((msg) => {
      const role = msg.role === "user" ? "User" : "Assistant";
      const content = msg.content.length > 2e3 ? msg.content.substring(0, 2e3) + "... [truncated]" : msg.content;
      return `${role}: ${content}`;
    }).join("\n\n");
    return `[PREVIOUS CONVERSATION CONTEXT]
The following is our previous conversation. Continue from where we left off:

${formattedMessages}

[END OF PREVIOUS CONTEXT]

`;
  }
  /**
   * Trim history to stay within limits
   */
  trimHistory() {
    while (this.messages.length > this.maxMessages) {
      this.messages.shift();
    }
    let totalChars = this.messages.reduce((sum, msg) => sum + msg.content.length, 0);
    while (totalChars > this.maxCharacters && this.messages.length > 1) {
      const removed = this.messages.shift();
      if (removed) {
        totalChars -= removed.content.length;
      }
    }
  }
  /**
   * Get a summary of the conversation for logging/debugging
   */
  getSummary() {
    const totalChars = this.messages.reduce((sum, msg) => sum + msg.content.length, 0);
    const userCount = this.messages.filter((m) => m.role === "user").length;
    const assistantCount = this.messages.filter((m) => m.role === "assistant").length;
    return `${this.messages.length} messages (${userCount} user, ${assistantCount} assistant), ${totalChars} chars`;
  }
}

async function runGemini(opts) {
  const sessionTag = randomUUID();
  connectionState.setBackend("Gemini");
  const api = await ApiClient.create(opts.credentials);
  const settings = await readSettings();
  const machineId = settings?.machineId;
  if (!machineId) {
    console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/ConsortiumAI/consortium-cli/issues`);
    process.exit(1);
  }
  logger.debug(`Using machineId: ${machineId}`);
  let cloudToken = void 0;
  let currentUserEmail = void 0;
  const fetchCloudToken = async () => {
    try {
      const vendorToken = await api.getVendorToken("gemini");
      if (vendorToken?.oauth?.access_token) {
        cloudToken = vendorToken.oauth.access_token;
        logger.debug("[Gemini] Using OAuth token from Consortium cloud");
        if (vendorToken.oauth.id_token) {
          try {
            const parts = vendorToken.oauth.id_token.split(".");
            if (parts.length === 3) {
              const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
              if (payload.email) {
                currentUserEmail = payload.email;
                logger.debug(`[Gemini] Current user email: ${currentUserEmail}`);
              }
            }
          } catch {
            logger.debug("[Gemini] Failed to decode id_token for email");
          }
        }
      }
    } catch (error) {
      logger.debug("[Gemini] Failed to fetch cloud token:", error);
    }
  };
  await Promise.all([
    api.getOrCreateMachine({
      machineId,
      metadata: initialMachineMetadata
    }),
    fetchCloudToken()
  ]);
  const { state, metadata } = createSessionMetadata({
    flavor: "gemini",
    machineId,
    startedBy: opts.startedBy
  });
  const activeOrgId = await ensureActiveOrg(opts.credentials);
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state, organizationId: activeOrgId ?? void 0, projectId: process.env.CONSORTIUM_PROJECT_ID || void 0 });
  let session;
  let permissionHandler;
  let isProcessingMessage = false;
  let pendingSessionSwap = null;
  const applyPendingSessionSwap = () => {
    if (pendingSessionSwap) {
      logger.debug("[gemini] Applying pending session swap");
      session = pendingSessionSwap;
      if (permissionHandler) {
        permissionHandler.updateSession(pendingSessionSwap);
      }
      pendingSessionSwap = null;
    }
  };
  const { session: initialSession, reconnectionHandle } = setupOfflineReconnection({
    organizationId: activeOrgId ?? void 0,
    api,
    sessionTag,
    metadata,
    state,
    response,
    onSessionSwap: (newSession) => {
      if (isProcessingMessage) {
        logger.debug("[gemini] Session swap requested during message processing - queueing");
        pendingSessionSwap = newSession;
      } else {
        session = newSession;
        if (permissionHandler) {
          permissionHandler.updateSession(newSession);
        }
      }
    }
  });
  session = initialSession;
  if (response) {
    try {
      logger.debug(`[START] Reporting session ${response.id} to daemon`);
      const result = await notifyDaemonSessionStarted(response.id, metadata);
      if (result.error) {
        logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
      } else {
        logger.debug(`[START] Reported session ${response.id} to daemon`);
      }
    } catch (error) {
      logger.debug("[START] Failed to report to daemon (may not be running):", error);
    }
  }
  const attachments = createRunnerAttachments({
    credentials: opts.credentials,
    sessionId: response?.id ?? sessionTag,
    logTag: "gemini",
    agentId: "gemini",
    getCurrentModel: () => currentModel ?? null
  });
  const messageQueue = new MessageQueue2((mode) => hashObject({
    permissionMode: mode.permissionMode,
    model: mode.model
  }));
  session.setQueueStatsProvider(() => messageQueue.stats());
  const conversationHistory = new ConversationHistory({ maxMessages: 20, maxCharacters: 5e4 });
  let currentPermissionMode = void 0;
  let currentModel = void 0;
  let hasStoredFirstMessage = false;
  session.onUserMessage((message) => {
    attachments.observeUserMessage(message);
    if (!hasStoredFirstMessage && message.content.text) {
      hasStoredFirstMessage = true;
      session.updateMetadata((m) => ({
        ...m,
        firstMessage: message.content.text.substring(0, 100)
      }));
    }
    let messagePermissionMode = currentPermissionMode;
    if (message.meta?.permissionMode) {
      const validModes = ["default", "read-only", "safe-yolo", "yolo"];
      if (validModes.includes(message.meta.permissionMode)) {
        messagePermissionMode = message.meta.permissionMode;
        currentPermissionMode = messagePermissionMode;
        updatePermissionMode(messagePermissionMode);
        logger.debug(`[Gemini] Permission mode updated from user message to: ${currentPermissionMode}`);
      } else {
        logger.debug(`[Gemini] Invalid permission mode received: ${message.meta.permissionMode}`);
      }
    } else {
      logger.debug(`[Gemini] User message received with no permission mode override, using current: ${currentPermissionMode ?? "default (effective)"}`);
    }
    if (currentPermissionMode === void 0) {
      currentPermissionMode = "default";
      updatePermissionMode("default");
    }
    const previousModel = currentModel;
    const resolvedModel = resolveModelFromMeta(message.meta, currentModel);
    const messageModel = resolvedModel.model;
    currentModel = resolvedModel.model;
    if (resolvedModel.changed) {
      if (resolvedModel.reset) {
        messageBuffer.addMessage("Model reset to default", "system");
        logger.debug(`[Gemini] Model reset to default (was ${previousModel})`);
      } else {
        updateDisplayedModel(messageModel, false);
        messageBuffer.addMessage(`Model changed to: ${messageModel}`, "system");
        logger.debug(`[Gemini] Model changed from ${previousModel} to ${messageModel}`);
      }
    }
    const originalUserMessage = message.content.text;
    let fullPrompt = originalUserMessage;
    if (isFirstMessage) {
      const systemParts = [ARTIFACT_SYSTEM_STEER, ATLAS_SYSTEM_STEER];
      if (message.meta?.appendSystemPrompt) systemParts.push(message.meta.appendSystemPrompt);
      fullPrompt = systemParts.join("\n\n") + "\n\n" + originalUserMessage;
      isFirstMessage = false;
    }
    const mode = {
      permissionMode: messagePermissionMode || "default",
      model: messageModel,
      originalUserMessage
      // Store original message separately
    };
    messageQueue.push(fullPrompt, mode);
    conversationHistory.addUserMessage(originalUserMessage);
  });
  let thinking = false;
  session.keepAlive(thinking, "remote");
  const keepAliveInterval = setInterval(() => {
    session.keepAlive(thinking, "remote");
  }, 2e3);
  session.updateAgentState((state2) => ({ ...state2, controlledByUser: false }));
  let isFirstMessage = true;
  const sendReady = () => {
    session.sendSessionEvent({ type: "ready" });
    try {
      api.push().sendToAllDevices(
        "It's ready!",
        "Gemini is waiting for your command",
        { sessionId: session.sessionId }
      );
    } catch (pushError) {
      logger.debug("[Gemini] Failed to send ready push", pushError);
    }
  };
  const emitReadyIfIdle = () => {
    if (shouldExit) {
      return false;
    }
    if (thinking) {
      return false;
    }
    if (isResponseInProgress) {
      return false;
    }
    if (messageQueue.size() > 0) {
      return false;
    }
    sendReady();
    return true;
  };
  let abortController = new AbortController();
  let shouldExit = false;
  let geminiBackend = null;
  let acpSessionId = null;
  let wasSessionCreated = false;
  async function handleAbort() {
    logger.debug("[Gemini] Abort requested - stopping current task");
    session.sendAgentMessage("gemini", {
      type: "turn_aborted",
      id: randomUUID()
    });
    reasoningProcessor.abort();
    diffProcessor.reset();
    try {
      abortController.abort();
      const pendingItems = messageQueue.queue;
      if (pendingItems.length > 0) {
        const latestMode = pendingItems[pendingItems.length - 1].mode;
        if (latestMode && latestMode.model !== currentModel) {
          logger.debug(`[Gemini] Preserving in-flight model across abort: ${currentModel ?? "default"} -> ${latestMode.model ?? "default"}`);
          currentModel = latestMode.model;
        }
      }
      messageQueue.reset();
      if (geminiBackend && acpSessionId) {
        await geminiBackend.cancel(acpSessionId);
      }
      logger.debug("[Gemini] Abort completed - session remains active");
    } catch (error) {
      logger.debug("[Gemini] Error during abort:", error);
    } finally {
      abortController = new AbortController();
    }
  }
  const handleKillSession = async () => {
    logger.debug("[Gemini] Kill session requested - terminating process");
    await handleAbort();
    logger.debug("[Gemini] Abort completed, proceeding with termination");
    try {
      if (session) {
        session.updateMetadata((currentMetadata) => ({
          ...currentMetadata,
          lifecycleState: "archived",
          lifecycleStateSince: Date.now(),
          archivedBy: "cli",
          archiveReason: "User terminated"
        }));
        session.sendSessionDeath();
        await session.flush();
        await session.close();
      }
      stopCaffeinate();
      consortiumServer.stop();
      if (geminiBackend) {
        await geminiBackend.dispose();
      }
      logger.debug("[Gemini] Session termination complete, exiting");
      process.exit(0);
    } catch (error) {
      logger.debug("[Gemini] Error during session termination:", error);
      process.exit(1);
    }
  };
  session.rpcHandlerManager.registerHandler("abort", handleAbort);
  registerKillSessionHandler(session.rpcHandlerManager, handleKillSession);
  const messageBuffer = new MessageBuffer();
  const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
  let inkInstance = null;
  let displayedModel = getInitialGeminiModel();
  const localConfig = readGeminiLocalConfig();
  logger.debug(`[gemini] Initial model setup: env[GEMINI_MODEL_ENV]=${process.env[GEMINI_MODEL_ENV] || "not set"}, localConfig=${localConfig.model || "not set"}, displayedModel=${displayedModel}`);
  const updateDisplayedModel = (model, _saveToConfig = false) => {
    if (model === void 0) {
      logger.debug(`[gemini] updateDisplayedModel called with undefined, skipping update`);
      return;
    }
    const oldModel = displayedModel;
    displayedModel = model;
    logger.debug(`[gemini] updateDisplayedModel called: oldModel=${oldModel}, newModel=${model}`);
    if (hasTTY && oldModel !== model) {
      logger.debug(`[gemini] Adding model update message to buffer: [MODEL:${model}]`);
      messageBuffer.addMessage(`[MODEL:${model}]`, "system");
    } else if (hasTTY) {
      logger.debug(`[gemini] Model unchanged, skipping update message`);
    }
  };
  if (hasTTY) {
    console.clear();
    const DisplayComponent = () => {
      const currentModelValue = displayedModel || "gemini-2.5-pro";
      return React.createElement(GeminiDisplay, {
        messageBuffer,
        logPath: process.env.DEBUG ? logger.getLogPath() : void 0,
        currentModel: currentModelValue,
        onExit: async () => {
          logger.debug("[gemini]: Exiting agent via Ctrl-C");
          shouldExit = true;
          await handleAbort();
        }
      });
    };
    inkInstance = render(React.createElement(DisplayComponent), {
      exitOnCtrlC: false,
      patchConsole: false
    });
    const initialModelName = displayedModel || "gemini-2.5-pro";
    logger.debug(`[gemini] Sending initial model to UI: ${initialModelName}`);
    messageBuffer.addMessage(`[MODEL:${initialModelName}]`, "system");
  }
  if (hasTTY) {
    process.stdin.resume();
    if (process.stdin.isTTY) {
      process.stdin.setRawMode(true);
    }
    process.stdin.setEncoding("utf8");
  }
  const consortiumServer = await startConsortiumServer(session, {
    sendArtifact: (msg) => session.sendAgentMessage("gemini", msg)
  });
  const bridgeCommand = join(projectPath(), "bin", "consortium-mcp.mjs");
  const mcpServers = {
    consortium: {
      command: bridgeCommand,
      args: ["--url", consortiumServer.url]
    }
  };
  await provisionAcpSession(process.cwd(), mcpServers, session.sessionId);
  permissionHandler = new GeminiPermissionHandler(session);
  const reasoningProcessor = new GeminiReasoningProcessor((message) => {
    session.sendAgentMessage("gemini", message);
  });
  const diffProcessor = new GeminiDiffProcessor((message) => {
    session.sendAgentMessage("gemini", message);
  });
  const updatePermissionMode = (mode) => {
    permissionHandler.setPermissionMode(mode);
  };
  let accumulatedResponse = "";
  let streamId = null;
  let answerSeq = 0;
  let reasoningSeq = 0;
  let isResponseInProgress = false;
  let sendPromptInFlight = false;
  let hadToolCallInTurn = false;
  let pendingChangeTitle = false;
  let changeTitleCompleted = false;
  let taskStartedSent = false;
  function setupGeminiMessageHandler(backend) {
    backend.onMessage((msg) => {
      switch (msg.type) {
        case "model-output":
          if (msg.textDelta) {
            if (!sendPromptInFlight) {
              session.sendAgentMessage("gemini", {
                type: "message",
                message: msg.textDelta
              });
              break;
            }
            if (!isResponseInProgress) {
              messageBuffer.removeLastMessage("system");
              messageBuffer.addMessage(msg.textDelta, "assistant");
              isResponseInProgress = true;
              logger.debug(`[gemini] Started new response, first chunk length: ${msg.textDelta.length}`);
            } else {
              messageBuffer.updateLastMessage(msg.textDelta, "assistant");
              logger.debug(`[gemini] Updated response, chunk length: ${msg.textDelta.length}, total accumulated: ${accumulatedResponse.length + msg.textDelta.length}`);
            }
            accumulatedResponse += msg.textDelta;
            if (streamId) session.sendStreamDelta("gemini", streamId, "answer", answerSeq++, msg.textDelta);
          }
          break;
        case "status":
          const statusDetail = msg.detail ? typeof msg.detail === "object" ? JSON.stringify(msg.detail) : String(msg.detail) : "";
          logger.debug(`[gemini] Status changed: ${msg.status}${statusDetail ? ` - ${statusDetail}` : ""}`);
          if (msg.status === "error") {
            logger.debug(`[gemini] \u26A0\uFE0F Error status received: ${statusDetail || "Unknown error"}`);
            session.sendAgentMessage("gemini", {
              type: "turn_aborted",
              id: randomUUID()
            });
          }
          if (msg.status === "running") {
            thinking = true;
            session.keepAlive(thinking, "remote");
            if (!taskStartedSent) {
              session.sendAgentMessage("gemini", {
                type: "task_started",
                id: randomUUID()
              });
              taskStartedSent = true;
            }
            messageBuffer.addMessage("Thinking...", "system");
          } else if (msg.status === "idle" || msg.status === "stopped") {
            reasoningProcessor.complete();
          } else if (msg.status === "error") {
            thinking = false;
            session.keepAlive(thinking, "remote");
            accumulatedResponse = "";
            isResponseInProgress = false;
            let errorMessage = "Unknown error";
            if (msg.detail) {
              if (typeof msg.detail === "object") {
                const detailObj = msg.detail;
                errorMessage = detailObj.message || detailObj.details || JSON.stringify(detailObj);
              } else {
                errorMessage = String(msg.detail);
              }
            }
            if (errorMessage.includes("Authentication required")) {
              errorMessage = `Authentication required.
For Google Workspace accounts, run: consortium gemini project set <project-id>
Or use a different Google account: consortium connect gemini
Guide: https://goo.gle/gemini-cli-auth-docs#workspace-gca`;
            }
            messageBuffer.addMessage(`Error: ${errorMessage}`, "status");
            session.sendAgentMessage("gemini", {
              type: "message",
              message: `Error: ${errorMessage}`
            });
          }
          break;
        case "tool-call":
          hadToolCallInTurn = true;
          const toolArgs = msg.args ? JSON.stringify(msg.args).substring(0, 100) : "";
          const isInvestigationTool = msg.toolName === "codebase_investigator" || typeof msg.toolName === "string" && msg.toolName.includes("investigator");
          logger.debug(`[gemini] \u{1F527} Tool call received: ${msg.toolName} (${msg.callId})${isInvestigationTool ? " [INVESTIGATION]" : ""}`);
          if (isInvestigationTool && msg.args && typeof msg.args === "object" && "objective" in msg.args) {
            logger.debug(`[gemini] \u{1F50D} Investigation objective: ${String(msg.args.objective).substring(0, 150)}...`);
          }
          messageBuffer.addMessage(`Executing: ${msg.toolName}${toolArgs ? ` ${toolArgs}${toolArgs.length >= 100 ? "..." : ""}` : ""}`, "tool");
          session.sendAgentMessage("gemini", {
            type: "tool-call",
            name: msg.toolName,
            callId: msg.callId,
            input: msg.args,
            id: randomUUID()
          });
          break;
        case "tool-result":
          if (msg.toolName === "change_title" || msg.callId?.includes("change_title") || msg.toolName === "consortium__change_title") {
            changeTitleCompleted = true;
            logger.debug("[gemini] change_title completed");
          }
          const isError = msg.result && typeof msg.result === "object" && "error" in msg.result;
          const resultText = typeof msg.result === "string" ? msg.result.substring(0, 200) : JSON.stringify(msg.result).substring(0, 200);
          const truncatedResult = resultText + (typeof msg.result === "string" && msg.result.length > 200 ? "..." : "");
          const resultSize = typeof msg.result === "string" ? msg.result.length : JSON.stringify(msg.result).length;
          logger.debug(`[gemini] ${isError ? "\u274C" : "\u2705"} Tool result received: ${msg.toolName} (${msg.callId}) - Size: ${resultSize} bytes${isError ? " [ERROR]" : ""}`);
          if (!isError) {
            diffProcessor.processToolResult(msg.toolName, msg.result, msg.callId);
          }
          if (isError) {
            const errorMsg = msg.result.error || "Tool call failed";
            logger.debug(`[gemini] \u274C Tool call error: ${errorMsg.substring(0, 300)}`);
            messageBuffer.addMessage(`Error: ${errorMsg}`, "status");
          } else {
            if (resultSize > 1e3) {
              logger.debug(`[gemini] \u2705 Large tool result (${resultSize} bytes) - first 200 chars: ${truncatedResult}`);
            }
            messageBuffer.addMessage(`Result: ${truncatedResult}`, "result");
          }
          session.sendAgentMessage("gemini", {
            type: "tool-result",
            callId: msg.callId,
            output: msg.result,
            id: randomUUID()
          });
          break;
        case "fs-edit":
          messageBuffer.addMessage(`File edit: ${msg.description}`, "tool");
          diffProcessor.processFsEdit(msg.path || "", msg.description, msg.diff);
          session.sendAgentMessage("gemini", {
            type: "file-edit",
            description: msg.description,
            diff: msg.diff,
            filePath: msg.path || "unknown",
            id: randomUUID()
          });
          break;
        default:
          if (msg.type === "token-count") {
            session.sendAgentMessage("gemini", {
              type: "token_count",
              ...msg,
              id: randomUUID()
            });
            try {
              const usage = usageFromAgentTokenCount(msg);
              if (usage) {
                session.sendUsageData(usage, currentModel, void 0, {
                  keyPrefix: "gemini",
                  providerId: "google",
                  zeroCost: true
                });
              }
            } catch (err) {
              logger.debug("[gemini] failed to emit token-count usage", err);
            }
          }
          break;
        case "terminal-output":
          messageBuffer.addMessage(msg.data, "result");
          session.sendAgentMessage("gemini", {
            type: "terminal-output",
            data: msg.data,
            callId: msg.callId || randomUUID()
          });
          break;
        case "permission-request":
          const payload = msg.payload || {};
          session.sendAgentMessage("gemini", {
            type: "permission-request",
            permissionId: msg.id,
            toolName: payload.toolName || msg.reason || "unknown",
            description: msg.reason || payload.toolName || "",
            options: payload
          });
          break;
        case "exec-approval-request":
          const execApprovalMsg = msg;
          const callId = execApprovalMsg.call_id || execApprovalMsg.callId || randomUUID();
          const { call_id, type, ...inputs } = execApprovalMsg;
          logger.debug(`[gemini] Exec approval request received: ${callId}`);
          messageBuffer.addMessage(`Exec approval requested: ${callId}`, "tool");
          session.sendAgentMessage("gemini", {
            type: "tool-call",
            name: "GeminiBash",
            // Similar to Codex's CodexBash
            callId,
            input: inputs,
            id: randomUUID()
          });
          break;
        case "patch-apply-begin":
          const patchBeginMsg = msg;
          const patchCallId = patchBeginMsg.call_id || patchBeginMsg.callId || randomUUID();
          const { call_id: patchCallIdVar, type: patchType, auto_approved, changes } = patchBeginMsg;
          const changeCount = changes ? Object.keys(changes).length : 0;
          const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
          messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
          logger.debug(`[gemini] Patch apply begin: ${patchCallId}, files: ${changeCount}`);
          session.sendAgentMessage("gemini", {
            type: "tool-call",
            name: "GeminiPatch",
            // Similar to Codex's CodexPatch
            callId: patchCallId,
            input: {
              auto_approved,
              changes
            },
            id: randomUUID()
          });
          break;
        case "patch-apply-end":
          const patchEndMsg = msg;
          const patchEndCallId = patchEndMsg.call_id || patchEndMsg.callId || randomUUID();
          const { call_id: patchEndCallIdVar, type: patchEndType, stdout, stderr, success } = patchEndMsg;
          if (success) {
            const message = stdout || "Files modified successfully";
            messageBuffer.addMessage(message.substring(0, 200), "result");
          } else {
            const errorMsg = stderr || "Failed to modify files";
            messageBuffer.addMessage(`Error: ${errorMsg.substring(0, 200)}`, "result");
          }
          logger.debug(`[gemini] Patch apply end: ${patchEndCallId}, success: ${success}`);
          session.sendAgentMessage("gemini", {
            type: "tool-result",
            callId: patchEndCallId,
            output: {
              stdout,
              stderr,
              success
            },
            id: randomUUID()
          });
          break;
        case "inline-media":
          (async () => {
            try {
              const bytes = Uint8Array.from(Buffer.from(msg.base64, "base64"));
              const uploaded = await session.uploadArtifact({ bytes, mime: msg.mime });
              session.sendAgentMessage("gemini", {
                type: "artifact",
                artifactId: uploaded.id,
                mime: uploaded.mime,
                name: msg.name,
                sizeBytes: uploaded.sizeBytes,
                id: randomUUID()
              });
            } catch (err) {
              logger.debug("[gemini] inline-media upload failed:", err);
            }
          })();
          break;
        case "event":
          if (msg.name === "thinking") {
            const thinkingPayload = msg.payload;
            const thinkingText = thinkingPayload && typeof thinkingPayload === "object" && "text" in thinkingPayload ? String(thinkingPayload.text || "") : "";
            if (thinkingText) {
              reasoningProcessor.processChunk(thinkingText);
              logger.debug(`[gemini] \u{1F4AD} Thinking chunk received: ${thinkingText.length} chars - Preview: ${thinkingText.substring(0, 100)}...`);
              if (!thinkingText.startsWith("**")) {
                const thinkingPreview = thinkingText.substring(0, 100);
                messageBuffer.updateLastMessage(`[Thinking] ${thinkingPreview}...`, "system");
              }
            }
            if (streamId && thinkingText) session.sendStreamDelta("gemini", streamId, "reasoning", reasoningSeq++, thinkingText);
          }
          break;
      }
    });
  }
  let eagerModel = null;
  const eagerBackendStart = (async () => {
    try {
      const backendResult = createGeminiBackend({
        cwd: process.cwd(),
        mcpServers,
        permissionHandler,
        cloudToken,
        currentUserEmail,
        model: void 0
      });
      setupGeminiMessageHandler(backendResult.backend);
      const acpT0 = Date.now();
      const { sessionId } = await backendResult.backend.startSession();
      const startupTimings = {
        acpHandshake: Date.now() - acpT0,
        readyTotal: Math.round(process.uptime() * 1e3)
      };
      geminiBackend = backendResult.backend;
      acpSessionId = sessionId;
      eagerModel = backendResult.model;
      conversationHistory.setCurrentModel(backendResult.model);
      updateDisplayedModel(backendResult.model, false);
      session.updateMetadata((m) => ({ ...m, agentSessionId: sessionId, startupTimings }));
      logger.debug(`[gemini] Warm ACP session started: ${sessionId} (model ${backendResult.model})`);
    } catch (error) {
      logger.debug("[gemini] Warm backend start failed \u2014 falling back to lazy start on first message:", error);
    }
  })();
  let first = true;
  try {
    let currentModeHash = null;
    let pending = null;
    while (!shouldExit) {
      let message = pending;
      pending = null;
      if (!message) {
        logger.debug("[gemini] Main loop: waiting for messages from queue...");
        const waitSignal = abortController.signal;
        const batch = await messageQueue.waitForMessagesAndGetAsString(waitSignal);
        if (!batch) {
          if (waitSignal.aborted && !shouldExit) {
            logger.debug("[gemini] Main loop: wait aborted, continuing...");
            continue;
          }
          logger.debug("[gemini] Main loop: no batch received, breaking...");
          break;
        }
        logger.debug(`[gemini] Main loop: received message from queue (length: ${batch.message.length})`);
        message = batch;
      }
      if (!message) {
        break;
      }
      let injectHistoryContext = false;
      if (wasSessionCreated && currentModeHash && message.hash !== currentModeHash) {
        logger.debug("[Gemini] Mode changed \u2013 restarting Gemini session");
        messageBuffer.addMessage("\u2550".repeat(40), "status");
        if (conversationHistory.hasHistory()) {
          messageBuffer.addMessage(`Switching model (preserving ${conversationHistory.size()} messages of context)...`, "status");
          injectHistoryContext = true;
          logger.debug(`[Gemini] Will inject conversation history: ${conversationHistory.getSummary()}`);
        } else {
          messageBuffer.addMessage("Starting new Gemini session (mode changed)...", "status");
        }
        permissionHandler.reset();
        reasoningProcessor.abort();
        if (geminiBackend) {
          await geminiBackend.dispose();
          geminiBackend = null;
        }
        const modelToUse = message.mode?.model === void 0 ? void 0 : message.mode.model || null;
        const backendResult = createGeminiBackend({
          cwd: process.cwd(),
          mcpServers,
          permissionHandler,
          cloudToken,
          currentUserEmail,
          // Pass model from message - if undefined, will use local config/env/default
          // If explicitly null, will skip local config and use env/default
          model: modelToUse
        });
        geminiBackend = backendResult.backend;
        setupGeminiMessageHandler(geminiBackend);
        const actualModel = backendResult.model;
        logger.debug(`[gemini] Model change - modelToUse=${modelToUse}, actualModel=${actualModel} (from ${backendResult.modelSource})`);
        conversationHistory.setCurrentModel(actualModel);
        logger.debug("[gemini] Starting new ACP session with model:", actualModel);
        const { sessionId } = await geminiBackend.startSession();
        acpSessionId = sessionId;
        logger.debug(`[gemini] New ACP session started: ${acpSessionId}`);
        logger.debug(`[gemini] Calling updateDisplayedModel with: ${actualModel}`);
        updateDisplayedModel(actualModel, false);
        updatePermissionMode(message.mode.permissionMode);
        wasSessionCreated = true;
        currentModeHash = message.hash;
        first = false;
      }
      currentModeHash = message.hash;
      const userMessageToShow = message.mode?.originalUserMessage || message.message;
      messageBuffer.addMessage(userMessageToShow, "user");
      isProcessingMessage = true;
      try {
        if (first || !wasSessionCreated) {
          await eagerBackendStart;
          if (geminiBackend && acpSessionId && !wasSessionCreated) {
            const modelToUse = message.mode?.model === void 0 ? void 0 : message.mode.model || null;
            if (modelToUse !== void 0 && modelToUse !== eagerModel) {
              logger.debug(`[gemini] First message requests model ${modelToUse}; rebuilding warm backend (was ${eagerModel})`);
              await geminiBackend.dispose();
              geminiBackend = null;
              acpSessionId = null;
            } else {
              updatePermissionMode(message.mode.permissionMode);
              wasSessionCreated = true;
              logger.debug(`[gemini] Adopted warm ACP session ${acpSessionId}`);
            }
          }
          if (!geminiBackend) {
            const modelToUse = message.mode?.model === void 0 ? void 0 : message.mode.model || null;
            const backendResult = createGeminiBackend({
              cwd: process.cwd(),
              mcpServers,
              permissionHandler,
              cloudToken,
              currentUserEmail,
              // Pass model from message - if undefined, will use local config/env/default
              // If explicitly null, will skip local config and use env/default
              model: modelToUse
            });
            geminiBackend = backendResult.backend;
            setupGeminiMessageHandler(geminiBackend);
            const actualModel = backendResult.model;
            logger.debug(`[gemini] Backend created, model will be: ${actualModel} (from ${backendResult.modelSource})`);
            logger.debug(`[gemini] Calling updateDisplayedModel with: ${actualModel}`);
            updateDisplayedModel(actualModel, false);
            conversationHistory.setCurrentModel(actualModel);
          }
          if (!acpSessionId) {
            logger.debug("[gemini] Starting ACP session...");
            updatePermissionMode(message.mode.permissionMode);
            const { sessionId } = await geminiBackend.startSession();
            acpSessionId = sessionId;
            logger.debug(`[gemini] ACP session started: ${acpSessionId}`);
            wasSessionCreated = true;
            currentModeHash = message.hash;
            session.updateMetadata((m) => ({ ...m, agentSessionId: acpSessionId ?? void 0 }));
            logger.debug(`[gemini] Displaying model in UI: ${displayedModel || "gemini-2.5-pro"}, displayedModel: ${displayedModel}`);
          }
        }
        if (!acpSessionId) {
          throw new Error("ACP session not started");
        }
        accumulatedResponse = "";
        streamId = randomUUID();
        answerSeq = 0;
        reasoningSeq = 0;
        isResponseInProgress = false;
        hadToolCallInTurn = false;
        taskStartedSent = false;
        sendPromptInFlight = true;
        pendingChangeTitle = message.message.includes("change_title") || message.message.includes("consortium__change_title");
        changeTitleCompleted = false;
        if (!geminiBackend || !acpSessionId) {
          throw new Error("Gemini backend or session not initialized");
        }
        let promptToSend = message.message;
        if (injectHistoryContext && conversationHistory.hasHistory()) {
          const historyContext = conversationHistory.getContextForNewSession();
          promptToSend = historyContext + promptToSend;
          logger.debug(`[gemini] Injected conversation history context (${historyContext.length} chars)`);
        }
        logger.debug(`[gemini] Sending prompt to Gemini (length: ${promptToSend.length}): ${promptToSend.substring(0, 100)}...`);
        logger.debug(`[gemini] Full prompt: ${promptToSend}`);
        const MAX_RETRIES = 3;
        const RETRY_DELAY_MS = 2e3;
        let lastError = null;
        for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
          try {
            attachments.bindBackend(geminiBackend);
            await attachments.sendPrompt(geminiBackend, acpSessionId, promptToSend);
            logger.debug("[gemini] Prompt sent successfully");
            if (geminiBackend.waitForResponseComplete) {
              await geminiBackend.waitForResponseComplete(12e4);
              logger.debug("[gemini] Response complete");
            }
            break;
          } catch (promptError) {
            lastError = promptError;
            const errObj = promptError;
            const errorDetails = errObj?.data?.details || errObj?.details || errObj?.message || "";
            const errorCode = errObj?.code;
            const isQuotaError = errorDetails.includes("exhausted") || errorDetails.includes("quota") || errorDetails.includes("capacity");
            if (isQuotaError) {
              const resetTimeMatch = errorDetails.match(/reset after (\d+h)?(\d+m)?(\d+s)?/i);
              let resetTimeMsg = "";
              if (resetTimeMatch) {
                const parts = resetTimeMatch.slice(1).filter(Boolean).join("");
                resetTimeMsg = ` Quota resets in ${parts}.`;
              }
              const quotaMsg = `Gemini quota exceeded.${resetTimeMsg} Try using a different model (gemini-2.5-flash-lite) or wait for quota reset.`;
              messageBuffer.addMessage(quotaMsg, "status");
              session.sendAgentMessage("gemini", { type: "message", message: quotaMsg });
              throw promptError;
            }
            const isEmptyResponseError = errorDetails.includes("empty response") || errorDetails.includes("Model stream ended");
            const isInternalError = errorCode === -32603;
            const isRetryable = isEmptyResponseError || isInternalError;
            if (isRetryable && attempt < MAX_RETRIES) {
              logger.debug(`[gemini] Retryable error on attempt ${attempt}/${MAX_RETRIES}: ${errorDetails}`);
              messageBuffer.addMessage(`Gemini returned empty response, retrying (${attempt}/${MAX_RETRIES})...`, "status");
              await new Promise((resolve2) => setTimeout(resolve2, RETRY_DELAY_MS * attempt));
              continue;
            }
            throw promptError;
          }
        }
        if (lastError && MAX_RETRIES > 1) {
          logger.debug("[gemini] Prompt succeeded after retries");
        }
        if (first) {
          first = false;
        }
      } catch (error) {
        logger.debug("[gemini] Error in gemini session:", error);
        const isAbortError = error instanceof Error && error.name === "AbortError";
        if (isAbortError) {
          messageBuffer.addMessage("Aborted by user", "status");
          session.sendSessionEvent({ type: "message", message: "Aborted by user" });
        } else {
          let errorMsg = "Process error occurred";
          if (typeof error === "object" && error !== null) {
            const errObj = error;
            const errorDetails = errObj.data?.details || errObj.details || "";
            const errorCode = errObj.code || errObj.status || errObj.response?.status;
            const errorMessage = errObj.message || errObj.error?.message || "";
            const errorString = String(error);
            if (errorCode === 404 || errorDetails.includes("notFound") || errorDetails.includes("404") || errorMessage.includes("not found") || errorMessage.includes("404")) {
              const currentModel2 = displayedModel || "gemini-2.5-pro";
              errorMsg = `Model "${currentModel2}" not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite`;
            } else if (errorCode === -32603 || errorDetails.includes("empty response") || errorDetails.includes("Model stream ended")) {
              errorMsg = "Gemini API returned empty response after retries. This is a temporary issue - please try again.";
            } else if (errorCode === 429 || errorDetails.includes("429") || errorMessage.includes("429") || errorString.includes("429") || errorDetails.includes("rateLimitExceeded") || errorDetails.includes("RESOURCE_EXHAUSTED") || errorMessage.includes("Rate limit exceeded") || errorMessage.includes("Resource exhausted") || errorString.includes("rateLimitExceeded") || errorString.includes("RESOURCE_EXHAUSTED")) {
              errorMsg = "Gemini API rate limit exceeded. Please wait a moment and try again. The API will retry automatically.";
            } else if (errorDetails.includes("quota") || errorMessage.includes("quota") || errorString.includes("quota") || errorDetails.includes("exhausted") || errorDetails.includes("capacity")) {
              const resetTimeMatch = (errorDetails + errorMessage + errorString).match(/reset after (\d+h)?(\d+m)?(\d+s)?/i);
              let resetTimeMsg = "";
              if (resetTimeMatch) {
                const parts = resetTimeMatch.slice(1).filter(Boolean).join("");
                resetTimeMsg = ` Quota resets in ${parts}.`;
              }
              errorMsg = `Gemini quota exceeded.${resetTimeMsg} Try using a different model (gemini-2.5-flash-lite) or wait for quota reset.`;
            } else if (errorMessage.includes("Authentication required") || errorDetails.includes("Authentication required") || errorCode === -32e3) {
              errorMsg = `Authentication required. For Google Workspace accounts, you need to set a Google Cloud Project:
  consortium gemini project set <your-project-id>
Or use a different Google account: consortium connect gemini
Guide: https://goo.gle/gemini-cli-auth-docs#workspace-gca`;
            } else if (Object.keys(error).length === 0) {
              errorMsg = 'Failed to start Gemini. Is "gemini" CLI installed? Run: npm install -g @google/gemini-cli';
            } else if (errObj.message || errorMessage) {
              errorMsg = errorDetails || errorMessage || errObj.message;
            }
          } else if (error instanceof Error) {
            errorMsg = error.message;
          }
          messageBuffer.addMessage(errorMsg, "status");
          session.sendAgentMessage("gemini", {
            type: "message",
            message: errorMsg
          });
        }
      } finally {
        sendPromptInFlight = false;
        permissionHandler.reset();
        reasoningProcessor.abort();
        diffProcessor.reset();
        if (accumulatedResponse.trim()) {
          const { text: messageText, options } = parseOptionsFromText(accumulatedResponse);
          conversationHistory.addAssistantMessage(messageText);
          let finalMessageText = messageText;
          if (options.length > 0) {
            const optionsXml = formatOptionsXml(options);
            finalMessageText = messageText + optionsXml;
            logger.debug(`[gemini] Found ${options.length} options in response:`, options);
          } else if (hasIncompleteOptions(accumulatedResponse)) {
            logger.debug(`[gemini] Warning: Incomplete options block detected`);
          }
          const messagePayload = {
            type: "message",
            message: finalMessageText,
            id: randomUUID(),
            ...options.length > 0 && { options }
          };
          logger.debug(`[gemini] Sending complete message to mobile (length: ${finalMessageText.length}): ${finalMessageText.substring(0, 100)}...`);
          session.sendAgentMessage("gemini", messagePayload);
          accumulatedResponse = "";
          isResponseInProgress = false;
        }
        if (streamId) {
          session.sendStreamDelta("gemini", streamId, "answer", answerSeq++, "", true);
          streamId = null;
        }
        session.sendAgentMessage("gemini", {
          type: "task_complete",
          id: randomUUID()
        });
        hadToolCallInTurn = false;
        pendingChangeTitle = false;
        changeTitleCompleted = false;
        taskStartedSent = false;
        thinking = false;
        session.keepAlive(thinking, "remote");
        emitReadyIfIdle();
        isProcessingMessage = false;
        applyPendingSessionSwap();
        logger.debug(`[gemini] Main loop: turn completed, continuing to next iteration (queue size: ${messageQueue.size()})`);
      }
    }
  } finally {
    logger.debug("[gemini]: Final cleanup start");
    if (reconnectionHandle) {
      logger.debug("[gemini]: Cancelling offline reconnection");
      reconnectionHandle.cancel();
    }
    try {
      session.sendSessionDeath();
      await session.flush();
      await session.close();
    } catch (e) {
      logger.debug("[gemini]: Error while closing session", e);
    }
    if (geminiBackend) {
      await geminiBackend.dispose();
    }
    try {
      await attachments.dispose();
    } catch (e) {
      logger.debug("[gemini]: Error disposing attachments scratch dir", e);
    }
    consortiumServer.stop();
    if (process.stdin.isTTY) {
      try {
        process.stdin.setRawMode(false);
      } catch {
      }
    }
    if (hasTTY) {
      try {
        process.stdin.pause();
      } catch {
      }
    }
    clearInterval(keepAliveInterval);
    if (inkInstance) {
      inkInstance.unmount();
    }
    messageBuffer.clear();
    logger.debug("[gemini]: Final cleanup completed");
  }
}

export { runGemini };