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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions