Skip to content

Shell Integration Warning when using MiMo: Root Cause Analysis and Fix #752

Description

@myk1yt

Shell Integration Warning when using MiMo: Root Cause Analysis and Fix

Environment

  • AI Model: MiMo 2.5 Pro (Xiaomi) — long Chain-of-Thought reasoning causes extended wait times between terminal commands, and tendency to maintain long sessions makes these bugs especially prominent
  • Zoo Code: v3.65.100202 (zoocodeorganization.zoo-code)
  • OS: Windows 11, PowerShell 5.1
  • Target File: dist/extension.js (15,442,677 bytes)
  • Date: 2026-06-28

Summary of Bugs Found

# Bug Severity User Experience Root Cause Function
1 Dead terminal reuse infinite loop 🔴 Critical Command execution permanently impossible (10+ consecutive failures) getOrCreateTerminal()
2 Missing shellPath fallback 🔴 Critical PowerShell cmdlets don't execute on Windows Ln constructor
3 execa provider cmd.exe fallback 🔴 Critical git commit and other commands with quotes fail xit.run()
4 Intermittent command failures (3 causes) 🟠 High Same command alternates between success/failure (~35% failure rate) getOrCreateTerminal(), catch block, Olr.execute()
5 Missing customCwd type validation 🟠 High Terminal state permanently corrupted, all commands fail CFi()

Bug 1: Dead Terminal Reuse Infinite Loop

Symptom

When executing commands via execute_command, if the terminal process is dead (user closed the panel, shell integration initialization failed, etc.), Zoo Code enters an infinite loop reusing the same dead terminal.

Command attempt → Terminal process already dead → ShellIntegrationError
→ Error handler retries with same taskId → getOrCreateTerminal returns same dead terminal
→ ShellIntegrationError again → infinite loop

Actual error message:

Command failed to execute in terminal due to a shell integration error.

VS Code UI warning:

Your command is being executed without VSCode terminal shell integration.

Real reproduction timeline (23 attempts in 16 minutes, 15 failures — 35% success rate):

[05:22:47] python analyze_script.py       → ✅ success
[05:23:07] powershell Get-ChildItem       → ❌ "shell integration error"
[05:23:25] python analyze_script.py       → ❌ "shell integration error"
[05:23:32] python analyze_script.py       → ❌ "shell integration error"
  ... (10 consecutive failures) ...
[05:24:49] python analyze_script.py       → ✅ success (new terminal assigned)
[05:25:11] python -c "..."                → ❌ "shell integration error"
[05:25:19] python -c "..."                → ✅ success
[05:25:26] python -c "..."                → ❌ "shell integration error"

Key observation: success/failure occurs regardless of command content. echo "test" succeeds, then python -c "print('test')" fails immediately after.

Root Cause Code (extension.js, getOrCreateTerminal(), offset ~14,921,404)

Call chain:

Olr.execute()                          [execute_command tool handler]
  └─ try { CFi(r, w) }                 [terminal command execution]
       ├─ Fh.getOrCreateTerminal(c, t.taskId, N)  [find or create terminal]
       └─ Y.runCommand(r, Q)            [execute command in terminal]
            └─ onNoShellIntegration → new Yq()   [ShellIntegrationError thrown]
  └─ catch(E)
       └─ FNl(E) → true → CFi(r, {...w, terminalShellIntegrationDisabled:!0})
                      ⚠️ Same dead terminal returned → infinite loop

getOrCreateTerminal()'s find() searches by taskId + provider + reuseKey + cwd, but dead terminals also satisfy all these conditions. There's no isClosed() check, so dead terminals are returned as-is.

// ❌ Original code — getOrCreateTerminal() (offset ~14,921,404)
static async getOrCreateTerminal(e, r, a="vscode") {
  let o = this.getAllTerminals(),
      s = a === "vscode" ? Ln.getReuseKey() : a,
      n;
  return r && (n = o.find(l => {
    if (l.busy || l.taskId !== r || l.provider !== a || l.reuseKey !== s) return !1;
    let c = l.getCurrentWorkingDirectory();
    return c ? jo(Rq.Uri.file(e).fsPath, c) : !1
  })),
  n || (n = o.find(l => {
    if (l.busy || l.provider !== a || l.reuseKey !== s) return !1;
    let c = l.getCurrentWorkingDirectory();
    return c ? jo(Rq.Uri.file(e).fsPath, c) : !1
  })),
  n || (n = this.createTerminal(e, a)),
  n.taskId = r,
  n
}

The error handler also doesn't create a new terminal on retry:

// ❌ Original code — catch block (offset ~15,077,884)
catch(E) {
  if (r.supersedePendingAsk(), FNl(E)) {
    let m = {executionId: v, status: "fallback"};
    x?.postMessageToWebview({type: "commandExecutionStatus", text: JSON.stringify(m)});
    let [g, y] = await CFi(r, {...w, terminalShellIntegrationDisabled: !0});
    // ⚠️ No forceNewTerminal option → getOrCreateTerminal returns same dead terminal
    g && (r.didRejectTool = !0), c(y)
  }
}

Fix (before/after)

Fix 1/5 — Add 4th parameter to getOrCreateTerminal():

// ✅ After fix — getOrCreateTerminal()
static async getOrCreateTerminal(e, r, a="vscode", __fn=!1) {
  let o = this.getAllTerminals(),
      s = a === "vscode" ? Ln.getReuseKey() : a,
      n;
  return __fn || (                    // ← When __fn is true, skip find() calls entirely
    r && (n = o.find(l => {
      if (l.busy || l.isClosed() || l.taskId !== r || l.provider !== a || l.reuseKey !== s) return !1;
      //                    ^^^^^^^^^^ Exclude dead terminals
      let c = l.getCurrentWorkingDirectory();
      return c ? jo(Rq.Uri.file(e).fsPath, c) : !1
    })),
    n || (n = o.find(l => {
      if (l.busy || l.isClosed() || l.provider !== a || l.reuseKey !== s) return !1;
      //                    ^^^^^^^^^^ Exclude dead terminals
      let c = l.getCurrentWorkingDirectory();
      return c ? jo(Rq.Uri.file(e).fsPath, c) : !1
    }))
  ),
  n || (n = this.createTerminal(e, a)),  // ← __fn=true always reaches here → new terminal
  n.taskId = r,
  n
}

Fix 2/5 — Add forceNewTerminal option to CFi() function (offset ~15,078,569):

// ❌ Original
async function CFi(t, {executionId:e, command:r, customCwd:a,
  terminalShellIntegrationDisabled:o=!0, commandExecutionTimeout:s=0, agentTimeout:n=0}) {

// ✅ After fix
async function CFi(t, {executionId:e, command:r, customCwd:a,
  terminalShellIntegrationDisabled:o=!0, commandExecutionTimeout:s=0, agentTimeout:n=0,
  forceNewTerminal:__fn=!1}) {

Fix 3/5 — Forward __fn to getOrCreateTerminal call (offset ~15,080,813):

// ❌ Original
let Y = await Fh.getOrCreateTerminal(c, t.taskId, N);

// ✅ After fix
let Y = await Fh.getOrCreateTerminal(c, t.taskId, N, __fn);

Fix 4/5 — Enable forceNewTerminal on error handler retry (offset ~15,078,012):

// ❌ Original
let [g, y] = await CFi(r, {...w, terminalShellIntegrationDisabled: !0});

// ✅ After fix
let [g, y] = await CFi(r, {...w, terminalShellIntegrationDisabled: !0, forceNewTerminal: !0});

Fix 5/5 — Include default value in initial options object (offset ~15,077,775):

// ❌ Original
agentTimeout: I};

// ✅ After fix
agentTimeout: I, forceNewTerminal: !1};

Bug 2: Missing shellPath Fallback

Symptom

On Windows, when the VS Code terminal profile is set to cmd.exe or no profile exists, Zoo Code doesn't set shellPath when creating a terminal, falling back to VS Code's default (cmd.exe). Since cmd.exe doesn't support shell integration, all commands fail.

Actual error messages:

Command: Test-Path "README.md"
Result: 'Test-Path' is not recognized as an internal or external command, operable program or batch file.
Command: if (Test-Path "README.md") { (Get-Item "README.md").Length }
Result: ")" was unexpected at this time.

Root Cause Code (extension.js, Ln constructor, offset ~13,190,853)

// ❌ Original code — Ln class constructor
var Ln = class t extends mD {
  terminal; cmdCounter = 0; activeShellExecution;
  constructor(e, r, a) {
    super("vscode", e, a, t.getReuseKey());
    let o = t.getEnv(), s = new Tf.ThemeIcon("rocket");
    if (r) this.terminal = r;
    else {
      let n = {cwd: a, name: "Zoo Code", iconPath: s, env: o},
          l = t.getProfileShell();
      l?.shellPath && (              // ← When shellPath is missing, this entire block is skipped
        n.shellPath = l.shellPath,
        l.shellArgs && (n.shellArgs = l.shellArgs),
        console.info(`[Terminal] Creating terminal with profile "${t.getTerminalProfile()}" -> ${l.shellPath}`),
        l.env && (n.env = {...l.env, ...o})
      ),
      this.terminal = Tf.window.createTerminal(n)
      // ⚠️ createTerminal called without n.shellPath set
      // → VS Code uses cmd.exe (Windows default)
      // → No shell integration support → all commands fail
    }
  }

Fix (before/after)

// ✅ After fix — Ln class constructor
l?.shellPath                     // ← Changed to ternary operator
? (                              // ← Profile has shellPath (preserve existing behavior)
  n.shellPath = l.shellPath,
  l.shellArgs && (n.shellArgs = l.shellArgs),
  console.info(`[Terminal] Creating terminal with profile "${t.getTerminalProfile()}" -> ${l.shellPath}`),
  l.env && (n.env = {...l.env, ...o})
)
: (                              // ← No shellPath: explicit fallback
  n.shellPath = process.platform === "win32"
    ? (process.env.SystemRoot || "C:\\Windows") + "\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
    : process.env.SHELL || "/bin/bash",
  console.info(`[Terminal] No profile shellPath found; using fallback: ${n.shellPath}`)
),
this.terminal = Tf.window.createTerminal(n)

Bug 3: execa Provider cmd.exe Fallback

Symptom

When the terminal provider is "execa" instead of "vscode", commands like git commit trigger the shell integration warning. Zoo Code AI detects: "The terminal appears to be running cmd.exe".

Root Cause Code (extension.js, xit.run(), offset ~14,916,022)

// ❌ Original code — execa shell path setting
shell: mD.getExecaShellPath() || !0
// When getExecaShellPath() is undefined → ||!0 → true
// execa with shell:true → Windows COMSPEC → cmd.exe

Fix (before/after)

// ❌ Original
shell: mD.getExecaShellPath() || !0

// ✅ After fix
shell: mD.getExecaShellPath() || (
  process.platform === "win32"
    ? (process.env.SystemRoot || "C:\\Windows") + "\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
    : process.env.SHELL || "/bin/bash"
)

Bug 4: Intermittent Command Failures (3 Causes)

Symptom

After fixing the above 3 bugs, command execution still intermittently fails (~35% failure rate). Same command alternates between success and failure regardless of content.

Cause 1: Missing isClosed() check in getOrCreateTerminal()

// ❌ Original — find() without isClosed() check
n = o.find(l => {
    if (l.busy || l.taskId !== r || ...) return !1;
    // ⚠️ No l.isClosed() check!
})

// ✅ After fix
n = o.find(l => {
    if (l.busy || l.isClosed() || l.taskId !== r || ...) return !1;
    //                    ^^^^^^^^^^ Added
})

Cause 2: Missing retry for non-Yq errors

// ❌ Original — catch block
catch(E) {
    if (FNl(E)) {
        await CFi(r, {...w, forceNewTerminal:!0});
    } else {
        // non-Yq error → warning only, no retry!
        await r.say("shell_integration_warning");
    }
}

// ✅ After fix
catch(E) {
    if (FNl(E)) {
        await CFi(r, {...w, forceNewTerminal:!0});
    } else if(!(E instanceof Yq)) {
        // non-Yq error also retries with new terminal
        await CFi(r, {...w, terminalShellIntegrationDisabled:!0, forceNewTerminal:!0});
    } else {
        await r.say("shell_integration_warning");
    }
}

Cause 3: terminalShellIntegrationDisabled default value problem

// ❌ Original — default true forces execa provider
{terminalShellIntegrationDisabled:N=!0}=R??{}

// ✅ After fix — default false uses vscode provider
{terminalShellIntegrationDisabled:N=!1}=R??{}

Bug 5: Missing customCwd Type Validation

Symptom

When customCwd is passed as an Object instead of string, TypeError occurs. Terminal state becomes permanently corrupted — all subsequent commands fail. Cannot recover even by reloading VS Code.

Stack Trace

TypeError: The "path" argument must be of type string. Received an instance of Object
    at Object.isAbsolute (node:path:482:5)
    at CFi (extension.js:5086:5100)
    at Olr.execute (extension.js:5086:4351)

Fix (before/after)

// ❌ Original
a ? cV.isAbsolute(a) ? c = a : c = cV.resolve(a) : ...

// ✅ After fix
a && typeof a === "string" ? cV.isAbsolute(a) ? c = a : c = cV.resolve(a) : ...

Impact Analysis: MiMo Model Relevance

These bugs affect all AI models. However, MiMo 2.5 Pro's characteristics make them more frequent and more visible:

  1. Long Chain-of-Thought: Extended wait times between terminal commands → higher shell integration timeout chance
  2. Iterative tool calls: Many steps → frequent terminal use → dead terminal accumulation
  3. Long sessions: 30+ minute sessions → failure rate spikes

GPT-4, Claude, Gemini, and other models experience the same bugs under the same conditions. The root cause is in Zoo Code's code, not the AI model.


Verification Results

Before fix (16 minutes): 23 attempts, 15 failures → 35% success rate

After fix: 10 consecutive commands tested, 9/10 success (90%). The 1 failure is a PowerShell 5.x && syntax limitation, not a Zoo Code bug. Zoo Code related errors: 0.


Official Fix Suggestions (TypeScript)

1. Terminal reuse state check

static async getOrCreateTerminal(cwd: string, taskId: string, provider: string = "vscode", forceNew: boolean = false) {
  const terminals = this.getAllTerminals();
  let terminal: TerminalWrapper | undefined;
  if (!forceNew) {
    terminal = terminals.find(t => {
      if (t.busy || t.isClosed() || t.taskId !== taskId || t.provider !== provider) return false;
      // ... cwd matching logic
    });
  }
  if (!terminal) terminal = this.createTerminal(cwd, provider);
  terminal.taskId = taskId;
  return terminal;
}

2. shellPath fallback chain

const profile = Terminal.getProfileShell();
if (profile?.shellPath) {
  options.shellPath = profile.shellPath;
} else {
  options.shellPath = process.platform === "win32"
    ? `${process.env.SystemRoot || "C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
    : process.env.SHELL || "/bin/bash";
}

3. execa provider shell path fallback

shell: ExecaTerminal.getExecaShellPath() || (
  process.platform === "win32"
    ? `${process.env.SystemRoot || "C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
    : process.env.SHELL || "/bin/bash"
)

4. Error handler non-Yq error retry

catch (error) {
  task.supersedePendingAsk();
  if (isShellIntegrationError(error) && !error.commandSubmitted) {
    // Retry with new terminal
    const [rejected, result] = await executeCommandInTerminal(task, {
      ...options, terminalShellIntegrationDisabled: true, forceNewTerminal: true,
    });
    resolve(result);
  } else if (!(error instanceof ShellIntegrationError)) {
    // Non-Yq errors also retry
    const [rejected, result] = await executeCommandInTerminal(task, {
      ...options, terminalShellIntegrationDisabled: true, forceNewTerminal: true,
    });
    resolve(result);
  } else {
    await task.say("shell_integration_warning");
    resolve("Command failed...");
  }
}

5. customCwd type validation

const cwd = customCwd && typeof customCwd === "string"
  ? (path.isAbsolute(customCwd) ? customCwd : path.resolve(customCwd))
  : defaultCwd;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions