flockbay 0.10.21 → 0.10.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  var ink = require('ink');
4
4
  var React = require('react');
5
- var types = require('./types-DeH24uWs.cjs');
5
+ var types = require('./types-DvlwEGpS.cjs');
6
6
  var index_js = require('@modelcontextprotocol/sdk/client/index.js');
7
7
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
8
8
  var z = require('zod');
@@ -12,7 +12,7 @@ var fs = require('node:fs');
12
12
  var os = require('node:os');
13
13
  var path = require('node:path');
14
14
  var node_child_process = require('node:child_process');
15
- var index = require('./index-Bhkn02hu.cjs');
15
+ var index = require('./index-BtB1Sqpy.cjs');
16
16
  require('axios');
17
17
  require('node:events');
18
18
  require('socket.io-client');
@@ -252,18 +252,18 @@ function buildMcpElicitationResult(decision, requestedSchemaRaw, options) {
252
252
  if (!raw) return "";
253
253
  if (/^Blocked\b/.test(raw)) return raw;
254
254
  if (raw === "ledger_read_required") {
255
- return "Blocked by policy (automatic; not the user): read the ledger before making file edits. Next: call mcp__flockbay__ledger_read, then retry the edit.";
255
+ return "Blocked by Policy (automatic; not the user): read the ledger before making file edits. Next: call mcp__flockbay__ledger_read, then retry the edit.";
256
256
  }
257
257
  if (raw === "docs_index_read_required") {
258
- return "Blocked by policy (automatic; not the user): read the game Documentation index before making edits. Next: call mcp__flockbay__docs_index_read, then retry the edit.";
258
+ return "Blocked by Policy (automatic; not the user): read the game Documentation index before making edits. Next: call mcp__flockbay__docs_index_read, then retry the edit.";
259
259
  }
260
260
  if (raw.startsWith("file_claim_required:")) {
261
261
  const withoutPrefix = raw.slice("file_claim_required:".length);
262
262
  const file = withoutPrefix.split("(")[0]?.trim() || "the file";
263
- return `Blocked by policy (automatic; not the user): claim ${file} before editing it. Next: claim the file via mcp__flockbay__ledger_claim (or mcp__flockbay__coordination_claim_files), then retry the edit.`;
263
+ return `Blocked by Policy (automatic; not the user): claim ${file} before editing it. Next: claim the file via mcp__flockbay__ledger_claim (or mcp__flockbay__coordination_claim_files), then retry the edit.`;
264
264
  }
265
265
  if (raw === "read_only_mode") {
266
- return "Blocked by policy (automatic; not the user): this session is in read-only mode. Next: switch permission mode to allow edits, then retry.";
266
+ return "Blocked by Policy (automatic; not the user): this session is in read-only mode. Next: switch permission mode to allow edits, then retry.";
267
267
  }
268
268
  return `Blocked: ${raw}`;
269
269
  };
@@ -1742,7 +1742,7 @@ function buildPolicyHint(reason, decision, gate) {
1742
1742
  };
1743
1743
  }
1744
1744
  return {
1745
- summary: "Blocked by policy.",
1745
+ summary: "Blocked by Policy.",
1746
1746
  nextSteps: []
1747
1747
  };
1748
1748
  }
@@ -1754,7 +1754,7 @@ function buildPolicyHint(reason, decision, gate) {
1754
1754
  }
1755
1755
  if (raw === "ledger_read_required") {
1756
1756
  return {
1757
- summary: "Blocked by policy (automatic; not the user): read the ledger before making file edits.",
1757
+ summary: "Blocked by Policy (automatic; not the user): read the ledger before making file edits.",
1758
1758
  nextSteps: [
1759
1759
  "Call `mcp__flockbay__ledger_read` (or `mcp__flockbay__coordination_ledger_snapshot`).",
1760
1760
  "Then retry the file edit."
@@ -1763,7 +1763,7 @@ function buildPolicyHint(reason, decision, gate) {
1763
1763
  }
1764
1764
  if (raw === "docs_index_read_required") {
1765
1765
  return {
1766
- summary: "Blocked by policy (automatic; not the user): read the game Documentation index before making edits.",
1766
+ summary: "Blocked by Policy (automatic; not the user): read the game Documentation index before making edits.",
1767
1767
  nextSteps: [
1768
1768
  "Call `mcp__flockbay__docs_index_read`.",
1769
1769
  "Then retry the edit."
@@ -1774,7 +1774,7 @@ function buildPolicyHint(reason, decision, gate) {
1774
1774
  const withoutPrefix = raw.slice("file_claim_required:".length);
1775
1775
  const file = withoutPrefix.split("(")[0]?.trim() || "the file";
1776
1776
  return {
1777
- summary: `Blocked by policy (automatic; not the user): claim \`${file}\` before editing it.`,
1777
+ summary: `Blocked by Policy (automatic; not the user): claim \`${file}\` before editing it.`,
1778
1778
  nextSteps: [
1779
1779
  `Claim the file via \`mcp__flockbay__ledger_claim\` or \`mcp__flockbay__coordination_claim_files\` (files: ["${file}"]).`,
1780
1780
  "Then retry the file edit."
@@ -1783,7 +1783,7 @@ function buildPolicyHint(reason, decision, gate) {
1783
1783
  }
1784
1784
  if (raw === "read_only_mode") {
1785
1785
  return {
1786
- summary: "Blocked by policy (automatic; not the user): this session is in read-only mode.",
1786
+ summary: "Blocked by Policy (automatic; not the user): this session is in read-only mode.",
1787
1787
  nextSteps: [
1788
1788
  "Switch permission mode to allow edits (disable read-only).",
1789
1789
  "Then retry the action."
@@ -2383,7 +2383,6 @@ async function runCodex(opts) {
2383
2383
  appendSystemPrompt: index.PLATFORM_SYSTEM_PROMPT
2384
2384
  });
2385
2385
  }
2386
- const bypassUeGates = String(process.env.FLOCKBAY_DEV_BYPASS_UE_GATES || "") === "1";
2387
2386
  let currentPermissionMode = void 0;
2388
2387
  let currentModel = void 0;
2389
2388
  const defaultAppendSystemPrompt = index.PLATFORM_SYSTEM_PROMPT;
@@ -3451,22 +3450,6 @@ Error: ${message}`,
3451
3450
  try {
3452
3451
  resetScreenshotGateForTurn();
3453
3452
  const overrides = { approvalPolicy: "untrusted", sandbox: "workspace-write" };
3454
- if (!bypassUeGates) {
3455
- const detection = await index.detectUnrealProject(process.cwd());
3456
- if (!detection.ok) {
3457
- session.sendCodexMessage({
3458
- type: "message",
3459
- message: "Select an Unreal project folder (the folder containing a *.uproject) to continue.",
3460
- id: node_crypto.randomUUID()
3461
- });
3462
- if (wasCreated) {
3463
- client.clearSession();
3464
- wasCreated = false;
3465
- currentModeHash = null;
3466
- }
3467
- continue;
3468
- }
3469
- }
3470
3453
  if (!wasCreated) {
3471
3454
  const projectCapsule = await index.buildProjectCapsule({ startDir: process.cwd() });
3472
3455
  const modelOverride = resolveCodexModelOverride(message.mode.model);
@@ -6,8 +6,8 @@ var node_crypto = require('node:crypto');
6
6
  var os = require('node:os');
7
7
  var path = require('node:path');
8
8
  var fs$2 = require('node:fs/promises');
9
- var types = require('./types-DeH24uWs.cjs');
10
- var index = require('./index-Bhkn02hu.cjs');
9
+ var types = require('./types-DvlwEGpS.cjs');
10
+ var index = require('./index-BtB1Sqpy.cjs');
11
11
  var node_child_process = require('node:child_process');
12
12
  var sdk = require('@agentclientprotocol/sdk');
13
13
  var fs = require('fs');
@@ -45,6 +45,17 @@ const KNOWN_TOOL_PATTERNS = {
45
45
  save_memory: ["save_memory", "save-memory"],
46
46
  think: ["think"]
47
47
  };
48
+ const MCP_FLOCKBAY_TOOL_RE = /\bmcp__flockbay__[a-z0-9_]+\b/gi;
49
+ const UNREAL_MCP_TOOL_RE = /\bunreal_mcp_[a-z0-9_]+\b/gi;
50
+ function extractFirstToolNameFromText(text) {
51
+ if (!text) return null;
52
+ const lower = text.toLowerCase();
53
+ const mcpMatch = lower.match(MCP_FLOCKBAY_TOOL_RE);
54
+ if (mcpMatch && mcpMatch[0]) return mcpMatch[0];
55
+ const unrealMatch = lower.match(UNREAL_MCP_TOOL_RE);
56
+ if (unrealMatch && unrealMatch[0]) return `mcp__flockbay__${unrealMatch[0]}`;
57
+ return null;
58
+ }
48
59
  function isInvestigationTool(toolCallId, toolKind) {
49
60
  return toolCallId.includes("codebase_investigator") || toolCallId.includes("investigator") || typeof toolKind === "string" && toolKind.includes("investigator");
50
61
  }
@@ -79,20 +90,30 @@ function determineToolName(toolName, toolCallId, input, params, context) {
79
90
  if (idToolName) {
80
91
  return idToolName;
81
92
  }
93
+ const idTextToolName = extractFirstToolNameFromText(toolCallId);
94
+ if (idTextToolName) {
95
+ return idTextToolName;
96
+ }
82
97
  if (input && typeof input === "object") {
83
- const inputStr = JSON.stringify(input).toLowerCase();
98
+ const inputStr = JSON.stringify(input);
99
+ const extracted = extractFirstToolNameFromText(inputStr);
100
+ if (extracted) return extracted;
101
+ const inputLower = inputStr.toLowerCase();
84
102
  for (const [toolName2, patterns] of Object.entries(KNOWN_TOOL_PATTERNS)) {
85
103
  for (const pattern of patterns) {
86
- if (inputStr.includes(pattern.toLowerCase())) {
104
+ if (inputLower.includes(pattern.toLowerCase())) {
87
105
  return toolName2;
88
106
  }
89
107
  }
90
108
  }
91
109
  }
92
- const paramsStr = JSON.stringify(params).toLowerCase();
110
+ const paramsStr = JSON.stringify(params);
111
+ const paramsExtracted = extractFirstToolNameFromText(paramsStr);
112
+ if (paramsExtracted) return paramsExtracted;
113
+ const paramsLower = paramsStr.toLowerCase();
93
114
  for (const [toolName2, patterns] of Object.entries(KNOWN_TOOL_PATTERNS)) {
94
115
  for (const pattern of patterns) {
95
- if (paramsStr.includes(pattern.toLowerCase())) {
116
+ if (paramsLower.includes(pattern.toLowerCase())) {
96
117
  return toolName2;
97
118
  }
98
119
  }
@@ -329,7 +350,7 @@ class AcpSdkBackend {
329
350
  this.emit({
330
351
  type: "status",
331
352
  status: "error",
332
- detail: "Model not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite"
353
+ detail: "Model not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-3-pro-preview, gemini-3-flash-preview"
333
354
  });
334
355
  } else if (/authentication required/i.test(text) || /login required/i.test(text) || /invalid.*(api key|key)/i.test(text) || /permission denied/i.test(text)) {
335
356
  const excerpt = this.stderrExcerpt(20);
@@ -1832,7 +1853,7 @@ class GeminiPermissionHandler {
1832
1853
  const decision = result.decision;
1833
1854
  const reason = result.reason;
1834
1855
  const kind = decision === "approved" || decision === "approved_for_session" ? "policy_allow" : decision === "abort" && reason === "permission_prompt_required" ? "policy_prompt" : "policy_block";
1835
- const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by policy.";
1856
+ const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by Policy.";
1836
1857
  const nextSteps = [];
1837
1858
  if (kind === "policy_block" && typeof reason === "string") {
1838
1859
  if (reason.includes("docs_index_read") || reason.includes("Documentation index")) nextSteps.push("Call `mcp__flockbay__docs_index_read`.");
@@ -2570,6 +2591,30 @@ ${transcript}`;
2570
2591
  types.logger.debug("[Gemini] Failed to send ready push", pushError);
2571
2592
  }
2572
2593
  };
2594
+ let pendingTurnCompletion = null;
2595
+ const beginTurnCompletionWait = () => {
2596
+ if (pendingTurnCompletion) {
2597
+ types.logger.debug("[Gemini] beginTurnCompletionWait called while a turn is already pending; replacing pending wait");
2598
+ try {
2599
+ pendingTurnCompletion.resolve("error");
2600
+ } catch {
2601
+ }
2602
+ pendingTurnCompletion = null;
2603
+ }
2604
+ let resolve2;
2605
+ const promise = new Promise((res) => {
2606
+ resolve2 = res;
2607
+ });
2608
+ pendingTurnCompletion = { sawRunning: false, resolve: resolve2, promise };
2609
+ return promise;
2610
+ };
2611
+ const maybeResolveTurnCompletion = (status) => {
2612
+ if (!pendingTurnCompletion) return;
2613
+ if (!pendingTurnCompletion.sawRunning) return;
2614
+ const pending = pendingTurnCompletion;
2615
+ pendingTurnCompletion = null;
2616
+ pending.resolve(status);
2617
+ };
2573
2618
  const emitReadyIfIdle = () => {
2574
2619
  if (shouldExit) {
2575
2620
  return false;
@@ -2597,10 +2642,36 @@ ${transcript}`;
2597
2642
  let wasSessionCreated = false;
2598
2643
  let abortNote = null;
2599
2644
  let abortNoteSentToSession = false;
2645
+ let abortRequested = false;
2646
+ let readySentForAbort = false;
2647
+ function normalizeCancelNote(input) {
2648
+ if (typeof input === "string") return input.trim() ? input.trim() : null;
2649
+ if (!input || typeof input !== "object") return null;
2650
+ const note = typeof input.note === "string" ? String(input.note).trim() : "";
2651
+ if (note) return note;
2652
+ const reason = typeof input.reason === "string" ? String(input.reason).trim() : "";
2653
+ if (reason) return reason;
2654
+ return null;
2655
+ }
2656
+ function makeAbortError(message = "Aborted") {
2657
+ const error = new Error(message);
2658
+ error.name = "AbortError";
2659
+ return error;
2660
+ }
2661
+ function abortable(signal, promise) {
2662
+ if (signal.aborted) return Promise.reject(makeAbortError());
2663
+ return new Promise((resolve2, reject) => {
2664
+ const onAbort = () => reject(makeAbortError());
2665
+ signal.addEventListener("abort", onAbort, { once: true });
2666
+ promise.then(resolve2, reject).finally(() => signal.removeEventListener("abort", onAbort));
2667
+ });
2668
+ }
2600
2669
  async function handleAbort(note, options) {
2601
2670
  types.logger.debug("[Gemini] Abort requested - stopping current task");
2602
- if (typeof note === "string" && note.trim()) {
2603
- abortNote = note.trim();
2671
+ abortRequested = true;
2672
+ const normalizedNote = normalizeCancelNote(note);
2673
+ if (normalizedNote) {
2674
+ abortNote = normalizedNote;
2604
2675
  abortNoteSentToSession = Boolean(options?.alreadySentToSession);
2605
2676
  }
2606
2677
  session.sendCodexMessage({
@@ -2611,6 +2682,13 @@ ${transcript}`;
2611
2682
  diffProcessor.reset();
2612
2683
  try {
2613
2684
  abortController.abort();
2685
+ if (pendingTurnCompletion) {
2686
+ try {
2687
+ pendingTurnCompletion.resolve("error");
2688
+ } catch {
2689
+ }
2690
+ pendingTurnCompletion = null;
2691
+ }
2614
2692
  messageQueue.reset();
2615
2693
  if (geminiBackend && acpSessionId) {
2616
2694
  await geminiBackend.cancel(acpSessionId);
@@ -2749,6 +2827,7 @@ ${transcript}`;
2749
2827
  };
2750
2828
  let accumulatedResponse = "";
2751
2829
  let isResponseInProgress = false;
2830
+ let currentResponseMessageId = null;
2752
2831
  function setupGeminiMessageHandler(backend) {
2753
2832
  backend.onMessage((msg) => {
2754
2833
  switch (msg.type) {
@@ -2778,6 +2857,9 @@ ${transcript}`;
2778
2857
  if (msg.status === "running") {
2779
2858
  thinking = true;
2780
2859
  session.keepAlive(thinking, "remote");
2860
+ if (pendingTurnCompletion) {
2861
+ pendingTurnCompletion.sawRunning = true;
2862
+ }
2781
2863
  session.sendCodexMessage({
2782
2864
  type: "task_started",
2783
2865
  id: node_crypto.randomUUID()
@@ -2822,11 +2904,14 @@ ${transcript}`;
2822
2904
  accumulatedResponse = "";
2823
2905
  isResponseInProgress = false;
2824
2906
  }
2907
+ maybeResolveTurnCompletion(msg.status);
2825
2908
  } else if (msg.status === "error") {
2826
2909
  thinking = false;
2827
2910
  session.keepAlive(thinking, "remote");
2828
2911
  accumulatedResponse = "";
2829
2912
  isResponseInProgress = false;
2913
+ currentResponseMessageId = null;
2914
+ maybeResolveTurnCompletion("error");
2830
2915
  const errorMessage = stringifyErrorish(msg.detail || "Unknown error");
2831
2916
  messageBuffer.addMessage(`Error: ${errorMessage}`, "status");
2832
2917
  session.sendCodexMessage({
@@ -3050,7 +3135,7 @@ ${transcript}`;
3050
3135
  const actualModel = determineGeminiModel(modelToUse, localConfigForModel);
3051
3136
  types.logger.debug(`[gemini] Model change - modelToUse=${modelToUse}, actualModel=${actualModel}`);
3052
3137
  types.logger.debug("[gemini] Starting new ACP session with model:", actualModel);
3053
- const { sessionId } = await geminiBackend.startSession();
3138
+ const { sessionId } = await abortable(abortController.signal, geminiBackend.startSession());
3054
3139
  acpSessionId = sessionId;
3055
3140
  startedSessionThisTurn = true;
3056
3141
  types.logger.debug(`[gemini] New ACP session started: ${acpSessionId}`);
@@ -3067,6 +3152,15 @@ ${transcript}`;
3067
3152
  let retryThisTurn = false;
3068
3153
  let skipAutoFinalize = false;
3069
3154
  try {
3155
+ const turnSignal = abortController.signal;
3156
+ const sendPromptAndWait = async (prompt) => {
3157
+ if (!geminiBackend || !acpSessionId) {
3158
+ throw new Error("Gemini backend or session not initialized");
3159
+ }
3160
+ const completion = beginTurnCompletionWait();
3161
+ await abortable(turnSignal, geminiBackend.sendPrompt(acpSessionId, prompt));
3162
+ return await abortable(turnSignal, completion);
3163
+ };
3070
3164
  if (first || !wasSessionCreated) {
3071
3165
  if (!geminiBackend) {
3072
3166
  await refreshGeminiCloudToken("before-backend-create");
@@ -3091,7 +3185,7 @@ ${transcript}`;
3091
3185
  if (!acpSessionId) {
3092
3186
  types.logger.debug("[gemini] Starting ACP session...");
3093
3187
  updatePermissionMode(message.mode.permissionMode);
3094
- const { sessionId } = await geminiBackend.startSession();
3188
+ const { sessionId } = await abortable(turnSignal, geminiBackend.startSession());
3095
3189
  acpSessionId = sessionId;
3096
3190
  startedSessionThisTurn = true;
3097
3191
  types.logger.debug(`[gemini] ACP session started: ${acpSessionId}`);
@@ -3132,18 +3226,27 @@ ${originalUserMessage}` : originalUserMessage;
3132
3226
  ...promptImages.map((img) => ({ type: "image", data: img.base64, mimeType: img.mimeType }))
3133
3227
  ];
3134
3228
  types.logger.debug(`[gemini] Sending multimodal prompt blocks (textLength=${promptText.length}, images=${promptImages.length})`);
3135
- await geminiBackend.sendPrompt(acpSessionId, blocks);
3229
+ const status = await sendPromptAndWait(blocks);
3230
+ if (status !== "idle") {
3231
+ skipAutoFinalize = true;
3232
+ }
3136
3233
  } else {
3137
3234
  types.logger.debug(`[gemini] Sending prompt to Gemini (length: ${promptText.length}): ${promptText.substring(0, 100)}...`);
3138
3235
  types.logger.debug(`[gemini] Full prompt: ${promptText}`);
3139
- await geminiBackend.sendPrompt(acpSessionId, promptText);
3236
+ const status = await sendPromptAndWait(promptText);
3237
+ if (status !== "idle") {
3238
+ skipAutoFinalize = true;
3239
+ }
3140
3240
  }
3141
3241
  types.logger.debug("[gemini] Prompt sent successfully");
3142
- if (!screenshotGate.inAutoReview && screenshotGate.paths.length > 0) {
3242
+ if (!skipAutoFinalize && !screenshotGate.inAutoReview && screenshotGate.paths.length > 0) {
3143
3243
  screenshotGate.inAutoReview = true;
3144
3244
  messageBuffer.addMessage("Auto-reviewing screenshots\u2026", "status");
3145
3245
  const blocks = await buildScreenshotReviewBlocks(screenshotGate.paths);
3146
- await geminiBackend.sendPrompt(acpSessionId, blocks);
3246
+ const status = await sendPromptAndWait(blocks);
3247
+ if (status !== "idle") {
3248
+ skipAutoFinalize = true;
3249
+ }
3147
3250
  screenshotGate.inAutoReview = false;
3148
3251
  }
3149
3252
  if (first) {
@@ -3152,14 +3255,21 @@ ${originalUserMessage}` : originalUserMessage;
3152
3255
  } catch (error) {
3153
3256
  types.logger.debug("[gemini] Error in gemini session:", error);
3154
3257
  const isAbortError = error instanceof Error && error.name === "AbortError";
3155
- if (isAbortError) {
3258
+ const treatAsAbort = abortRequested || isAbortError;
3259
+ if (treatAsAbort) {
3156
3260
  skipAutoFinalize = true;
3157
- const note = abortNote || "Aborted by user";
3261
+ readySentForAbort = true;
3262
+ const note = abortNote || "Canceled by user";
3158
3263
  abortNote = null;
3159
3264
  const alreadySent = abortNoteSentToSession;
3160
3265
  abortNoteSentToSession = false;
3266
+ abortRequested = false;
3267
+ accumulatedResponse = "";
3268
+ isResponseInProgress = false;
3269
+ currentResponseMessageId = null;
3161
3270
  messageBuffer.addMessage(note, "status");
3162
3271
  if (!alreadySent) session.sendSessionEvent({ type: "message", message: note });
3272
+ session.sendSessionEvent({ type: "ready" });
3163
3273
  } else {
3164
3274
  const rawErrorString = error instanceof Error ? error.message : String(error);
3165
3275
  const looksLikeAuthOrQuotaError = /unauthenticated|unauthorized|invalid (api )?key|api key not valid|permission denied|forbidden|expired|quota|usage limit|rate limit|resource exhausted|resource_exhausted|status 401|status 403|status 429|\b401\b|\b403\b|\b429\b/i.test(rawErrorString);
@@ -3191,7 +3301,7 @@ ${originalUserMessage}` : originalUserMessage;
3191
3301
  const errorString = String(error);
3192
3302
  if (errorCode === 404 || errorDetails.includes("notFound") || errorDetails.includes("404") || errorMessage.includes("not found") || errorMessage.includes("404")) {
3193
3303
  const currentModel2 = displayedModel || "gemini-2.5-pro";
3194
- errorMsg = `Model "${currentModel2}" not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite`;
3304
+ errorMsg = `Model "${currentModel2}" not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-3-pro-preview, gemini-3-flash-preview`;
3195
3305
  } 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")) {
3196
3306
  errorMsg = "Gemini API rate limit exceeded. Please wait a moment and try again. The API will retry automatically.";
3197
3307
  } else if (errorDetails.includes("quota") || errorMessage.includes("quota") || errorString.includes("quota")) {
@@ -3215,6 +3325,7 @@ ${originalUserMessage}` : originalUserMessage;
3215
3325
  permissionHandler.reset();
3216
3326
  reasoningProcessor.abort();
3217
3327
  diffProcessor.reset();
3328
+ abortRequested = false;
3218
3329
  thinking = false;
3219
3330
  session.keepAlive(thinking, "remote");
3220
3331
  if (!retryThisTurn) {
@@ -3244,7 +3355,11 @@ ${originalUserMessage}` : originalUserMessage;
3244
3355
  });
3245
3356
  }
3246
3357
  }
3247
- emitReadyIfIdle();
3358
+ if (readySentForAbort) {
3359
+ readySentForAbort = false;
3360
+ } else {
3361
+ emitReadyIfIdle();
3362
+ }
3248
3363
  }
3249
3364
  types.logger.debug(`[gemini] Main loop: turn completed, continuing to next iteration (queue size: ${messageQueue.size()})`);
3250
3365
  }