From 08936076946dc95b0f74b1106d44da8c23dbc1ae Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:01:33 +0000 Subject: [PATCH 01/17] Fix bash tool to use runtime-appropriate temp directory - Changed overflow file path from config.tempDir to {cwd}/.cmux/tmp/ - This fixes SSH runtime support where config.tempDir was a local path - Runtime.writeFile() expects runtime-appropriate paths (remote for SSH) - Updated test to verify files are created in .cmux/tmp subdirectory --- src/services/tools/bash.test.ts | 9 ++++++--- src/services/tools/bash.ts | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index 0542280b2..02c323fd6 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -3,6 +3,7 @@ import { createBashTool } from "./bash"; import type { BashToolArgs, BashToolResult } from "@/types/tools"; import { BASH_MAX_TOTAL_BYTES } from "@/constants/toolLimits"; import * as fs from "fs"; +import * as path from "path"; import { TestTempDir } from "./testHelpers"; import { createRuntime } from "@/runtime/runtimeFactory"; @@ -269,7 +270,7 @@ describe("bash tool", () => { it("should use tmpfile policy by default when overflow_policy not specified", async () => { const tempDir = new TestTempDir("test-bash-default"); const tool = createBashTool({ - cwd: process.cwd(), + cwd: tempDir.path, // Use tempDir as cwd so we can verify file creation runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), tempDir: tempDir.path, // overflow_policy not specified - should default to tmpfile @@ -289,8 +290,10 @@ describe("bash tool", () => { expect(result.error).toContain("saved to"); expect(result.error).not.toContain("[OUTPUT TRUNCATED"); - // Verify temp file was created - const files = fs.readdirSync(tempDir.path); + // Verify temp file was created in .cmux/tmp subdirectory + const cmuxTmpDir = path.join(tempDir.path, ".cmux", "tmp"); + expect(fs.existsSync(cmuxTmpDir)).toBe(true); + const files = fs.readdirSync(cmuxTmpDir); const bashFiles = files.filter((f) => f.startsWith("bash-")); expect(bashFiles.length).toBe(1); } diff --git a/src/services/tools/bash.ts b/src/services/tools/bash.ts index 8def72db6..e4ddb72bf 100644 --- a/src/services/tools/bash.ts +++ b/src/services/tools/bash.ts @@ -419,7 +419,9 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { try { // Use 8 hex characters for short, memorable temp file IDs const fileId = Math.random().toString(16).substring(2, 10); - const overflowPath = path.join(config.tempDir, `bash-${fileId}.txt`); + // Write to .cmux/tmp directory relative to cwd for runtime-agnostic path + // This works for both LocalRuntime (local path) and SSHRuntime (remote path) + const overflowPath = path.join(config.cwd, ".cmux", "tmp", `bash-${fileId}.txt`); const fullOutput = lines.join("\n"); // Use runtime.writeFile() for SSH support From 09fcb9206b6750568ddb8fb280414b844771e199 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:27:47 +0000 Subject: [PATCH 02/17] Fix bash overflow temp directory to use Runtime abstraction The bash tool overflow feature writes large outputs to temp files when they exceed display limits. The previous implementation broke with SSH runtimes because temp directories were created using local filesystem operations (fs.mkdirSync) but passed to runtime.writeFile() which needs runtime-appropriate paths. Changes: - Renamed 'tempDir' to 'runtimeTempDir' throughout for clarity - StreamManager.createTempDirForStream() now uses runtime.exec('mkdir -p') - Stream cleanup now uses runtime.exec('rm -rf') for temp directory removal - Pass Runtime parameter through startStream() to enable temp dir operations - Store Runtime in WorkspaceStreamInfo for cleanup access - Updated ToolConfiguration interface (tempDir -> runtimeTempDir) - Updated all tests to use runtimeTempDir This ensures temp directories are created and cleaned up in the correct location (local for LocalRuntime, remote for SSHRuntime) while preserving the existing cleanup guarantees when streams end. --- src/services/aiService.ts | 8 +++-- src/services/streamManager.ts | 62 +++++++++++++++++++++++---------- src/services/tools/bash.test.ts | 22 ++++++------ src/services/tools/bash.ts | 4 +-- src/utils/tools/tools.ts | 4 +-- 5 files changed, 64 insertions(+), 36 deletions(-) diff --git a/src/services/aiService.ts b/src/services/aiService.ts index fe15e1ca2..29729b906 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -523,14 +523,17 @@ export class AIService extends EventEmitter { // Generate stream token and create temp directory for tools const streamToken = this.streamManager.generateStreamToken(); - const tempDir = this.streamManager.createTempDirForStream(streamToken); + const runtimeTempDir = await this.streamManager.createTempDirForStream( + streamToken, + runtime + ); // Get model-specific tools with workspace path (correct for local or remote) const allTools = await getToolsForModel(modelString, { cwd: workspacePath, runtime, secrets: secretsToRecord(projectSecrets), - tempDir, + runtimeTempDir, }); // Apply tool policy to filter tools (if policy provided) @@ -695,6 +698,7 @@ export class AIService extends EventEmitter { modelString, historySequence, systemMessage, + runtime, abortSignal, tools, { diff --git a/src/services/streamManager.ts b/src/services/streamManager.ts index fc9a656a9..c72fea603 100644 --- a/src/services/streamManager.ts +++ b/src/services/streamManager.ts @@ -31,6 +31,7 @@ import type { HistoryService } from "./historyService"; import { AsyncMutex } from "@/utils/concurrency/asyncMutex"; import type { ToolPolicy } from "@/utils/tools/toolPolicy"; import { StreamingTokenTracker } from "@/utils/main/StreamingTokenTracker"; +import type { Runtime } from "@/runtime/Runtime"; // Type definitions for stream parts with extended properties interface ReasoningDeltaPart { @@ -107,7 +108,9 @@ interface WorkspaceStreamInfo { // Track background processing promise for guaranteed cleanup processingPromise: Promise; // Temporary directory for tool outputs (auto-cleaned when stream ends) - tempDir: string; + runtimeTempDir: string; + // Runtime for temp directory cleanup + runtime: Runtime; } /** @@ -242,11 +245,24 @@ export class StreamManager extends EventEmitter { * - Agent mistakes when copying/manipulating paths * - Harder to read in tool outputs * - Potential path length issues on some systems + * + * Uses the Runtime abstraction so temp directories work for both local and SSH runtimes. */ - public createTempDirForStream(streamToken: StreamToken): string { - const homeDir = os.homedir(); - const tempDir = path.join(homeDir, ".cmux-tmp", streamToken); - fs.mkdirSync(tempDir, { recursive: true, mode: 0o700 }); + public async createTempDirForStream( + streamToken: StreamToken, + runtime: Runtime + ): Promise { + const tempDir = path.join("~", ".cmux-tmp", streamToken); + // Use runtime.exec() to create directory (works for both local and remote) + const result = await runtime.exec(`mkdir -p "${tempDir}"`, { + cwd: "~", + timeout: 10, + }); + // Wait for command to complete + const exitCode = await result.exitCode; + if (exitCode !== 0) { + throw new Error(`Failed to create temp directory ${tempDir}: exit code ${exitCode}`); + } return tempDir; } @@ -429,7 +445,8 @@ export class StreamManager extends EventEmitter { private createStreamAtomically( workspaceId: WorkspaceId, streamToken: StreamToken, - tempDir: string, + runtimeTempDir: string, + runtime: Runtime, messages: ModelMessage[], model: LanguageModel, modelString: string, @@ -508,7 +525,8 @@ export class StreamManager extends EventEmitter { lastPartialWriteTime: 0, // Initialize to 0 to allow immediate first write partialWritePromise: undefined, // No write in flight initially processingPromise: Promise.resolve(), // Placeholder, overwritten in startStream - tempDir, // Stream-scoped temp directory for tool outputs + runtimeTempDir, // Stream-scoped temp directory for tool outputs + runtime, // Runtime for temp directory cleanup }; // Atomically register the stream @@ -961,13 +979,20 @@ export class StreamManager extends EventEmitter { streamInfo.partialWriteTimer = undefined; } - // Clean up stream temp directory - if (streamInfo.tempDir && fs.existsSync(streamInfo.tempDir)) { + // Clean up stream temp directory using runtime + if (streamInfo.runtimeTempDir) { try { - fs.rmSync(streamInfo.tempDir, { recursive: true, force: true }); - log.debug(`Cleaned up temp dir: ${streamInfo.tempDir}`); + const result = await streamInfo.runtime.exec( + `rm -rf "${streamInfo.runtimeTempDir}"`, + { + cwd: "~", + timeout: 10, + } + ); + await result.exitCode; // Wait for completion + log.debug(`Cleaned up temp dir: ${streamInfo.runtimeTempDir}`); } catch (error) { - log.error(`Failed to cleanup temp dir ${streamInfo.tempDir}:`, error); + log.error(`Failed to cleanup temp dir ${streamInfo.runtimeTempDir}:`, error); // Don't throw - cleanup is best-effort } } @@ -1090,6 +1115,7 @@ export class StreamManager extends EventEmitter { modelString: string, historySequence: number, system: string, + runtime: Runtime, abortSignal?: AbortSignal, tools?: Record, initialMetadata?: Partial, @@ -1123,18 +1149,16 @@ export class StreamManager extends EventEmitter { // Step 2: Use provided stream token or generate a new one const streamToken = providedStreamToken ?? this.generateStreamToken(); - // Step 3: Create temp directory for this stream - // If token was provided, temp dir might already exist - that's fine - const tempDir = path.join(os.homedir(), ".cmux-tmp", streamToken); - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, { recursive: true, mode: 0o700 }); - } + // Step 3: Create temp directory for this stream using runtime + // If token was provided, temp dir might already exist - mkdir -p handles this + const runtimeTempDir = await this.createTempDirForStream(streamToken, runtime); // Step 4: Atomic stream creation and registration const streamInfo = this.createStreamAtomically( typedWorkspaceId, streamToken, - tempDir, + runtimeTempDir, + runtime, messages, model, modelString, diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index 02c323fd6..03c9387ea 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -23,7 +23,7 @@ function createTestBashTool(options?: { niceness?: number }) { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, ...options, }); @@ -165,7 +165,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, overflow_policy: "truncate", }); @@ -204,7 +204,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, overflow_policy: "truncate", }); @@ -236,7 +236,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, overflow_policy: "truncate", }); @@ -272,7 +272,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: tempDir.path, // Use tempDir as cwd so we can verify file creation runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, // overflow_policy not specified - should default to tmpfile }); @@ -306,7 +306,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, }); // Generate ~50KB of output (well over 16KB display limit, under 100KB file limit) @@ -358,7 +358,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, }); // Generate ~150KB of output (exceeds 100KB file limit) @@ -401,7 +401,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, }); // Generate output that exceeds display limit but not file limit @@ -443,7 +443,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, }); // Generate a single line exceeding 1KB limit, then try to output more @@ -483,7 +483,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, }); // Generate ~15KB of output (just under 16KB display limit) @@ -513,7 +513,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, }); // Generate exactly 300 lines (hits line limit exactly) diff --git a/src/services/tools/bash.ts b/src/services/tools/bash.ts index e4ddb72bf..160973863 100644 --- a/src/services/tools/bash.ts +++ b/src/services/tools/bash.ts @@ -419,9 +419,9 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { try { // Use 8 hex characters for short, memorable temp file IDs const fileId = Math.random().toString(16).substring(2, 10); - // Write to .cmux/tmp directory relative to cwd for runtime-agnostic path + // Write to runtime temp directory (managed by StreamManager) // This works for both LocalRuntime (local path) and SSHRuntime (remote path) - const overflowPath = path.join(config.cwd, ".cmux", "tmp", `bash-${fileId}.txt`); + const overflowPath = path.join(config.runtimeTempDir, `bash-${fileId}.txt`); const fullOutput = lines.join("\n"); // Use runtime.writeFile() for SSH support diff --git a/src/utils/tools/tools.ts b/src/utils/tools/tools.ts index 952abebef..a876a17a8 100644 --- a/src/utils/tools/tools.ts +++ b/src/utils/tools/tools.ts @@ -22,8 +22,8 @@ export interface ToolConfiguration { secrets?: Record; /** Process niceness level (optional, -20 to 19, lower = higher priority) */ niceness?: number; - /** Temporary directory for tool outputs (required) */ - tempDir: string; + /** Temporary directory for tool outputs in runtime's context (local or remote) */ + runtimeTempDir: string; /** Overflow policy for bash tool output (optional, not exposed to AI) */ overflow_policy?: "truncate" | "tmpfile"; } From 2a3218fbefdb3be48f2077c740e17a36f02642eb Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:38:55 +0000 Subject: [PATCH 03/17] Fix test to check runtimeTempDir instead of .cmux/tmp subdirectory The test 'should use tmpfile policy by default' was checking for files in .cmux/tmp subdirectory, which was the path used in the initial (incorrect) fix. Now that overflow files are written directly to runtimeTempDir, the test needs to check the tempDir.path directly. --- src/services/tools/bash.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index 03c9387ea..84db35a81 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -270,7 +270,7 @@ describe("bash tool", () => { it("should use tmpfile policy by default when overflow_policy not specified", async () => { const tempDir = new TestTempDir("test-bash-default"); const tool = createBashTool({ - cwd: tempDir.path, // Use tempDir as cwd so we can verify file creation + cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), runtimeTempDir: tempDir.path, // overflow_policy not specified - should default to tmpfile @@ -290,10 +290,9 @@ describe("bash tool", () => { expect(result.error).toContain("saved to"); expect(result.error).not.toContain("[OUTPUT TRUNCATED"); - // Verify temp file was created in .cmux/tmp subdirectory - const cmuxTmpDir = path.join(tempDir.path, ".cmux", "tmp"); - expect(fs.existsSync(cmuxTmpDir)).toBe(true); - const files = fs.readdirSync(cmuxTmpDir); + // Verify temp file was created in runtimeTempDir + expect(fs.existsSync(tempDir.path)).toBe(true); + const files = fs.readdirSync(tempDir.path); const bashFiles = files.filter((f) => f.startsWith("bash-")); expect(bashFiles.length).toBe(1); } From 422be7512c278c4005ca5c051d983132b24e9c85 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:43:19 +0000 Subject: [PATCH 04/17] Fix path expansion: return absolute path from createTempDirForStream The previous implementation returned '~/.cmux-tmp/{token}' which is not an absolute path. This would fail when passed to runtime.writeFile() in LocalRuntime, as fs operations don't expand tilde. Solution: Use 'mkdir -p ~/.cmux-tmp/{token} && cd ... && pwd' to get the absolute path after creation. This works for both LocalRuntime and SSHRuntime: - LocalRuntime: returns '/home/user/.cmux-tmp/{token}' - SSHRuntime: returns '/home/remoteuser/.cmux-tmp/{token}' (remote path) Also switched to execBuffered() helper for cleaner code. --- src/services/streamManager.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/services/streamManager.ts b/src/services/streamManager.ts index c72fea603..d1895a7b2 100644 --- a/src/services/streamManager.ts +++ b/src/services/streamManager.ts @@ -32,6 +32,7 @@ import { AsyncMutex } from "@/utils/concurrency/asyncMutex"; import type { ToolPolicy } from "@/utils/tools/toolPolicy"; import { StreamingTokenTracker } from "@/utils/main/StreamingTokenTracker"; import type { Runtime } from "@/runtime/Runtime"; +import { execBuffered } from "@/utils/runtime/helpers"; // Type definitions for stream parts with extended properties interface ReasoningDeltaPart { @@ -252,18 +253,22 @@ export class StreamManager extends EventEmitter { streamToken: StreamToken, runtime: Runtime ): Promise { - const tempDir = path.join("~", ".cmux-tmp", streamToken); - // Use runtime.exec() to create directory (works for both local and remote) - const result = await runtime.exec(`mkdir -p "${tempDir}"`, { - cwd: "~", + // Create directory and get absolute path (works for both local and remote) + // Use 'cd' + 'pwd' to resolve ~ to absolute path + const command = `mkdir -p ~/.cmux-tmp/${streamToken} && cd ~/.cmux-tmp/${streamToken} && pwd`; + const result = await execBuffered(runtime, command, { + cwd: "/", timeout: 10, }); - // Wait for command to complete - const exitCode = await result.exitCode; - if (exitCode !== 0) { - throw new Error(`Failed to create temp directory ${tempDir}: exit code ${exitCode}`); + + if (result.exitCode !== 0) { + throw new Error( + `Failed to create temp directory ~/.cmux-tmp/${streamToken}: exit code ${result.exitCode}` + ); } - return tempDir; + + // Return absolute path (e.g., "/home/user/.cmux-tmp/abc123") + return result.stdout.trim(); } /** From 5a88fc44633e8c386c90a7cee45f92bb8a3326b1 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:42:20 +0000 Subject: [PATCH 05/17] Fix remaining tempDir references to use runtimeTempDir TypeScript compilation errors revealed four places that still used tempDir: - aiService.ts: Early tool creation for sentinel detection - ipcMain.ts: executeBash IPC handler - todo.ts: Todo write tool (2 locations) All now use runtimeTempDir to match the ToolConfiguration interface. --- src/services/aiService.ts | 2 +- src/services/ipcMain.ts | 2 +- src/services/tools/todo.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/aiService.ts b/src/services/aiService.ts index 29729b906..d06f75055 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -425,7 +425,7 @@ export class AIService extends EventEmitter { const earlyAllTools = await getToolsForModel(modelString, { cwd: process.cwd(), runtime: earlyRuntime, - tempDir: os.tmpdir(), + runtimeTempDir: os.tmpdir(), secrets: {}, }); const earlyTools = applyToolPolicy(earlyAllTools, toolPolicy); diff --git a/src/services/ipcMain.ts b/src/services/ipcMain.ts index d32fc4f2e..16bf340b5 100644 --- a/src/services/ipcMain.ts +++ b/src/services/ipcMain.ts @@ -900,7 +900,7 @@ export class IpcMain { runtime, secrets: secretsToRecord(projectSecrets), niceness: options?.niceness, - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, overflow_policy: "truncate", }); diff --git a/src/services/tools/todo.ts b/src/services/tools/todo.ts index aedb96cf9..8794c46ce 100644 --- a/src/services/tools/todo.ts +++ b/src/services/tools/todo.ts @@ -116,7 +116,7 @@ export const createTodoWriteTool: ToolFactory = (config) => { description: TOOL_DEFINITIONS.todo_write.description, inputSchema: TOOL_DEFINITIONS.todo_write.schema, execute: async ({ todos }) => { - await writeTodos(config.tempDir, todos); + await writeTodos(config.runtimeTempDir, todos); return { success: true as const, count: todos.length, @@ -134,7 +134,7 @@ export const createTodoReadTool: ToolFactory = (config) => { description: TOOL_DEFINITIONS.todo_read.description, inputSchema: TOOL_DEFINITIONS.todo_read.schema, execute: async () => { - const todos = await readTodos(config.tempDir); + const todos = await readTodos(config.runtimeTempDir); return { todos, }; From e7b5580af23a13c2f43cc65ad1859c0f920d1259 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:42:58 +0000 Subject: [PATCH 06/17] Use path.posix.join for overflow path to preserve POSIX separators Fixes Windows + SSH runtime compatibility. config.runtimeTempDir is always a POSIX path (e.g., /home/user/.cmux-tmp/token) even when cmux runs on Windows. Using path.join() would convert it to backslash form on Windows, breaking SSHRuntime.writeFile() which expects forward slashes. Solution: Always use path.posix.join() when constructing paths for runtime operations, regardless of host OS. Addresses Codex review comment PRRT_kwDOPxxmWM5fYEbF --- src/services/tools/bash.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/tools/bash.ts b/src/services/tools/bash.ts index 160973863..00618fa85 100644 --- a/src/services/tools/bash.ts +++ b/src/services/tools/bash.ts @@ -420,8 +420,9 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { // Use 8 hex characters for short, memorable temp file IDs const fileId = Math.random().toString(16).substring(2, 10); // Write to runtime temp directory (managed by StreamManager) - // This works for both LocalRuntime (local path) and SSHRuntime (remote path) - const overflowPath = path.join(config.runtimeTempDir, `bash-${fileId}.txt`); + // Use path.posix.join to preserve forward slashes for SSH runtime + // (config.runtimeTempDir is always a POSIX path like /home/user/.cmux-tmp/token) + const overflowPath = path.posix.join(config.runtimeTempDir, `bash-${fileId}.txt`); const fullOutput = lines.join("\n"); // Use runtime.writeFile() for SSH support From 0004073143e173c784e996391b9bfb281d83f429 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:44:28 +0000 Subject: [PATCH 07/17] Remove unused imports from streamManager.ts Removed unused imports: fs, path (first occurrence), os These were left over after switching to execBuffered() for temp dir creation. --- src/services/streamManager.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/services/streamManager.ts b/src/services/streamManager.ts index d1895a7b2..14c39a269 100644 --- a/src/services/streamManager.ts +++ b/src/services/streamManager.ts @@ -1,7 +1,4 @@ import { EventEmitter } from "events"; -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; import { streamText, stepCountIs, From 435c677a00de778cd8bf2b742b20fc3a332abeb6 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:46:10 +0000 Subject: [PATCH 08/17] Remove unused path import from bash.test.ts --- src/services/tools/bash.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index 84db35a81..89c44ad3b 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -3,7 +3,6 @@ import { createBashTool } from "./bash"; import type { BashToolArgs, BashToolResult } from "@/types/tools"; import { BASH_MAX_TOTAL_BYTES } from "@/constants/toolLimits"; import * as fs from "fs"; -import * as path from "path"; import { TestTempDir } from "./testHelpers"; import { createRuntime } from "@/runtime/runtimeFactory"; From d21fd2e549fc495a5f586067698e9d1a69666f15 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:48:15 +0000 Subject: [PATCH 09/17] Fix all test files to use runtimeTempDir and pass runtime parameter - Replaced all tempDir: with runtimeTempDir: in test files - Added runtime parameter to all streamManager.startStream() calls in tests - Imported createRuntime() and created runtime instance for tests --- src/services/streamManager.test.ts | 3 +++ src/services/tools/file_edit_insert.test.ts | 8 ++++---- src/services/tools/file_edit_operation.test.ts | 2 +- src/services/tools/file_edit_replace.test.ts | 4 ++-- src/services/tools/file_read.test.ts | 4 ++-- src/services/tools/todo.test.ts | 2 +- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/services/streamManager.test.ts b/src/services/streamManager.test.ts index 18342d493..c3d97b9f4 100644 --- a/src/services/streamManager.test.ts +++ b/src/services/streamManager.test.ts @@ -4,6 +4,7 @@ import type { HistoryService } from "./historyService"; import type { PartialService } from "./partialService"; import { createAnthropic } from "@ai-sdk/anthropic"; import { shouldRunIntegrationTests, validateApiKeys } from "../../tests/testUtils"; +import { createRuntime } from "@/runtime/runtimeFactory"; // Skip integration tests if TEST_INTEGRATION is not set const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip; @@ -85,6 +86,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 1, "You are a helpful assistant", + runtime, undefined, {} ); @@ -102,6 +104,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 2, "You are a helpful assistant", + runtime, undefined, {} ); diff --git a/src/services/tools/file_edit_insert.test.ts b/src/services/tools/file_edit_insert.test.ts index b9353e80b..562f50be1 100644 --- a/src/services/tools/file_edit_insert.test.ts +++ b/src/services/tools/file_edit_insert.test.ts @@ -21,7 +21,7 @@ function createTestFileEditInsertTool(options?: { cwd?: string }) { const tool = createFileEditInsertTool({ cwd: options?.cwd ?? process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, }); return { @@ -214,7 +214,7 @@ describe("file_edit_insert tool", () => { const tool = createFileEditInsertTool({ cwd: testDir, runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: "/tmp", + runtimeTempDir: "/tmp", }); const args: FileEditInsertToolArgs = { file_path: nonExistentPath, @@ -240,7 +240,7 @@ describe("file_edit_insert tool", () => { const tool = createFileEditInsertTool({ cwd: testDir, runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: "/tmp", + runtimeTempDir: "/tmp", }); const args: FileEditInsertToolArgs = { file_path: nestedPath, @@ -267,7 +267,7 @@ describe("file_edit_insert tool", () => { const tool = createFileEditInsertTool({ cwd: testDir, runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: "/tmp", + runtimeTempDir: "/tmp", }); const args: FileEditInsertToolArgs = { file_path: testFilePath, diff --git a/src/services/tools/file_edit_operation.test.ts b/src/services/tools/file_edit_operation.test.ts index ffb90a1af..c8510aa6f 100644 --- a/src/services/tools/file_edit_operation.test.ts +++ b/src/services/tools/file_edit_operation.test.ts @@ -10,7 +10,7 @@ function createConfig(runtime?: Runtime) { return { cwd: TEST_CWD, runtime: runtime ?? createRuntime({ type: "local", srcBaseDir: TEST_CWD }), - tempDir: "/tmp", + runtimeTempDir: "/tmp", }; } diff --git a/src/services/tools/file_edit_replace.test.ts b/src/services/tools/file_edit_replace.test.ts index 0082028b4..6f3d62217 100644 --- a/src/services/tools/file_edit_replace.test.ts +++ b/src/services/tools/file_edit_replace.test.ts @@ -60,7 +60,7 @@ describe("file_edit_replace_string tool", () => { const tool = createFileEditReplaceStringTool({ cwd: testDir, runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: "/tmp", + runtimeTempDir: "/tmp", }); const payload: FileEditReplaceStringToolArgs = { @@ -98,7 +98,7 @@ describe("file_edit_replace_lines tool", () => { const tool = createFileEditReplaceLinesTool({ cwd: testDir, runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: "/tmp", + runtimeTempDir: "/tmp", }); const payload: FileEditReplaceLinesToolArgs = { diff --git a/src/services/tools/file_read.test.ts b/src/services/tools/file_read.test.ts index 69a28fe69..5bd855ade 100644 --- a/src/services/tools/file_read.test.ts +++ b/src/services/tools/file_read.test.ts @@ -21,7 +21,7 @@ function createTestFileReadTool(options?: { cwd?: string }) { const tool = createFileReadTool({ cwd: options?.cwd ?? process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, }); return { @@ -335,7 +335,7 @@ describe("file_read tool", () => { const tool = createFileReadTool({ cwd: subDir, runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: "/tmp", + runtimeTempDir: "/tmp", }); const args: FileReadToolArgs = { filePath: "../test.txt", // This goes outside subDir back to testDir diff --git a/src/services/tools/todo.test.ts b/src/services/tools/todo.test.ts index f343d578d..95c789562 100644 --- a/src/services/tools/todo.test.ts +++ b/src/services/tools/todo.test.ts @@ -6,7 +6,7 @@ import { clearTodosForTempDir, getTodosForTempDir, setTodosForTempDir } from "./ import type { TodoItem } from "@/types/tools"; describe("Todo Storage", () => { - let tempDir: string; + let runtimeTempDir: string; beforeEach(async () => { // Create a temporary directory for each test From d4ff8160eff4bdd6ed68a7c5e4991ab8119dd1a7 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:50:11 +0000 Subject: [PATCH 10/17] Fix eslint errors in test files - Added runtime constant to StreamManager tests - Fixed todo.test.ts to use runtimeTempDir variable instead of tempDir --- src/services/streamManager.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/streamManager.test.ts b/src/services/streamManager.test.ts index c3d97b9f4..84474704f 100644 --- a/src/services/streamManager.test.ts +++ b/src/services/streamManager.test.ts @@ -39,6 +39,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { let streamManager: StreamManager; let mockHistoryService: HistoryService; let mockPartialService: PartialService; + const runtime = createRuntime({ type: "local", srcBaseDir: "/tmp" }); beforeEach(() => { mockHistoryService = createMockHistoryService(); From 3795366d3f7751f15c204c05564455af3c9c3a2b Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:50:22 +0000 Subject: [PATCH 11/17] Fix todo.test.ts to use runtimeTempDir consistently --- src/services/tools/todo.test.ts | 56 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/services/tools/todo.test.ts b/src/services/tools/todo.test.ts index 95c789562..206d13a72 100644 --- a/src/services/tools/todo.test.ts +++ b/src/services/tools/todo.test.ts @@ -10,12 +10,12 @@ describe("Todo Storage", () => { beforeEach(async () => { // Create a temporary directory for each test - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "todo-test-")); + runtimeTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "todo-test-")); }); afterEach(async () => { // Clean up temporary directory after each test - await fs.rm(tempDir, { recursive: true, force: true }); + await fs.rm(runtimeTempDir, { recursive: true, force: true }); }); describe("setTodosForTempDir", () => { @@ -35,9 +35,9 @@ describe("Todo Storage", () => { }, ]; - await setTodosForTempDir(tempDir, todos); + await setTodosForTempDir(runtimeTempDir, todos); - const storedTodos = await getTodosForTempDir(tempDir); + const storedTodos = await getTodosForTempDir(runtimeTempDir); expect(storedTodos).toEqual(todos); }); @@ -54,7 +54,7 @@ describe("Todo Storage", () => { }, ]; - await setTodosForTempDir(tempDir, initialTodos); + await setTodosForTempDir(runtimeTempDir, initialTodos); // Replace with updated list const updatedTodos: TodoItem[] = [ @@ -72,16 +72,16 @@ describe("Todo Storage", () => { }, ]; - await setTodosForTempDir(tempDir, updatedTodos); + await setTodosForTempDir(runtimeTempDir, updatedTodos); // Verify list was replaced, not merged - const storedTodos = await getTodosForTempDir(tempDir); + const storedTodos = await getTodosForTempDir(runtimeTempDir); expect(storedTodos).toEqual(updatedTodos); }); it("should handle empty todo list", async () => { // Create initial list - await setTodosForTempDir(tempDir, [ + await setTodosForTempDir(runtimeTempDir, [ { content: "Task 1", status: "pending", @@ -89,9 +89,9 @@ describe("Todo Storage", () => { ]); // Clear list - await setTodosForTempDir(tempDir, []); + await setTodosForTempDir(runtimeTempDir, []); - const storedTodos = await getTodosForTempDir(tempDir); + const storedTodos = await getTodosForTempDir(runtimeTempDir); expect(storedTodos).toEqual([]); }); @@ -108,10 +108,10 @@ describe("Todo Storage", () => { { content: "Task 8", status: "pending" }, ]; - await expect(setTodosForTempDir(tempDir, tooManyTodos)).rejects.toThrow( + await expect(setTodosForTempDir(runtimeTempDir, tooManyTodos)).rejects.toThrow( /Too many TODOs \(8\/7\)/i ); - await expect(setTodosForTempDir(tempDir, tooManyTodos)).rejects.toThrow( + await expect(setTodosForTempDir(runtimeTempDir, tooManyTodos)).rejects.toThrow( /Keep high precision at the center/i ); }); @@ -127,8 +127,8 @@ describe("Todo Storage", () => { { content: "Future work (5 items)", status: "pending" }, ]; - await setTodosForTempDir(tempDir, maxTodos); - expect(await getTodosForTempDir(tempDir)).toEqual(maxTodos); + await setTodosForTempDir(runtimeTempDir, maxTodos); + expect(await getTodosForTempDir(runtimeTempDir)).toEqual(maxTodos); }); it("should reject multiple in_progress tasks", async () => { @@ -139,7 +139,7 @@ describe("Todo Storage", () => { }, ]; - await setTodosForTempDir(tempDir, validTodos); + await setTodosForTempDir(runtimeTempDir, validTodos); const invalidTodos: TodoItem[] = [ { @@ -152,12 +152,12 @@ describe("Todo Storage", () => { }, ]; - await expect(setTodosForTempDir(tempDir, invalidTodos)).rejects.toThrow( + await expect(setTodosForTempDir(runtimeTempDir, invalidTodos)).rejects.toThrow( /only one task can be marked as in_progress/i ); // Original todos should remain unchanged on failure - expect(await getTodosForTempDir(tempDir)).toEqual(validTodos); + expect(await getTodosForTempDir(runtimeTempDir)).toEqual(validTodos); }); it("should reject when in_progress tasks appear after pending", async () => { @@ -172,7 +172,7 @@ describe("Todo Storage", () => { }, ]; - await expect(setTodosForTempDir(tempDir, invalidTodos)).rejects.toThrow( + await expect(setTodosForTempDir(runtimeTempDir, invalidTodos)).rejects.toThrow( /in-progress tasks must appear before pending tasks/i ); }); @@ -189,7 +189,7 @@ describe("Todo Storage", () => { }, ]; - await expect(setTodosForTempDir(tempDir, invalidTodos)).rejects.toThrow( + await expect(setTodosForTempDir(runtimeTempDir, invalidTodos)).rejects.toThrow( /completed tasks must appear before in-progress or pending tasks/i ); }); @@ -206,14 +206,14 @@ describe("Todo Storage", () => { }, ]; - await setTodosForTempDir(tempDir, todos); - expect(await getTodosForTempDir(tempDir)).toEqual(todos); + await setTodosForTempDir(runtimeTempDir, todos); + expect(await getTodosForTempDir(runtimeTempDir)).toEqual(todos); }); }); describe("getTodosForTempDir", () => { it("should return empty array when no todos exist", async () => { - const todos = await getTodosForTempDir(tempDir); + const todos = await getTodosForTempDir(runtimeTempDir); expect(todos).toEqual([]); }); @@ -229,9 +229,9 @@ describe("Todo Storage", () => { }, ]; - await setTodosForTempDir(tempDir, todos); + await setTodosForTempDir(runtimeTempDir, todos); - const retrievedTodos = await getTodosForTempDir(tempDir); + const retrievedTodos = await getTodosForTempDir(runtimeTempDir); expect(retrievedTodos).toEqual(todos); }); }); @@ -283,11 +283,11 @@ describe("Todo Storage", () => { }, ]; - await setTodosForTempDir(tempDir, todos); - expect(await getTodosForTempDir(tempDir)).toEqual(todos); + await setTodosForTempDir(runtimeTempDir, todos); + expect(await getTodosForTempDir(runtimeTempDir)).toEqual(todos); - await clearTodosForTempDir(tempDir); - expect(await getTodosForTempDir(tempDir)).toEqual([]); + await clearTodosForTempDir(runtimeTempDir); + expect(await getTodosForTempDir(runtimeTempDir)).toEqual([]); }); }); }); From 3e4bf7402de7a09e2693ed41cc453705060059b1 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:51:50 +0000 Subject: [PATCH 12/17] Fix remaining test issues: runtime params and tempDir references --- src/services/streamManager.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/streamManager.test.ts b/src/services/streamManager.test.ts index 84474704f..003864dac 100644 --- a/src/services/streamManager.test.ts +++ b/src/services/streamManager.test.ts @@ -287,6 +287,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 2, "system", + runtime, undefined, {} ), @@ -297,6 +298,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 3, "system", + runtime, undefined, {} ), From 27e67746e9da0d12703d1798babeb71dc83c0fc8 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:53:53 +0000 Subject: [PATCH 13/17] Add runtime parameter to first startStream call in test --- src/services/streamManager.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/streamManager.test.ts b/src/services/streamManager.test.ts index 003864dac..0ceb575a7 100644 --- a/src/services/streamManager.test.ts +++ b/src/services/streamManager.test.ts @@ -277,6 +277,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 1, "system", + runtime, undefined, {} ), From 9422efdb3fcb70c43e93061e5d1951009e2ea09f Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:55:59 +0000 Subject: [PATCH 14/17] Trigger CI rerun From 49314d45b9c2175af9d77e2d60a66f795fdebc94 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:59:43 +0000 Subject: [PATCH 15/17] Run prettier formatting on aiService.ts and streamManager.ts --- src/services/aiService.ts | 5 +---- src/services/streamManager.ts | 22 ++++++++-------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/services/aiService.ts b/src/services/aiService.ts index d06f75055..cdf89d0a3 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -523,10 +523,7 @@ export class AIService extends EventEmitter { // Generate stream token and create temp directory for tools const streamToken = this.streamManager.generateStreamToken(); - const runtimeTempDir = await this.streamManager.createTempDirForStream( - streamToken, - runtime - ); + const runtimeTempDir = await this.streamManager.createTempDirForStream(streamToken, runtime); // Get model-specific tools with workspace path (correct for local or remote) const allTools = await getToolsForModel(modelString, { diff --git a/src/services/streamManager.ts b/src/services/streamManager.ts index 14c39a269..5a05b9af6 100644 --- a/src/services/streamManager.ts +++ b/src/services/streamManager.ts @@ -243,13 +243,10 @@ export class StreamManager extends EventEmitter { * - Agent mistakes when copying/manipulating paths * - Harder to read in tool outputs * - Potential path length issues on some systems - * + * * Uses the Runtime abstraction so temp directories work for both local and SSH runtimes. */ - public async createTempDirForStream( - streamToken: StreamToken, - runtime: Runtime - ): Promise { + public async createTempDirForStream(streamToken: StreamToken, runtime: Runtime): Promise { // Create directory and get absolute path (works for both local and remote) // Use 'cd' + 'pwd' to resolve ~ to absolute path const command = `mkdir -p ~/.cmux-tmp/${streamToken} && cd ~/.cmux-tmp/${streamToken} && pwd`; @@ -257,13 +254,13 @@ export class StreamManager extends EventEmitter { cwd: "/", timeout: 10, }); - + if (result.exitCode !== 0) { throw new Error( `Failed to create temp directory ~/.cmux-tmp/${streamToken}: exit code ${result.exitCode}` ); } - + // Return absolute path (e.g., "/home/user/.cmux-tmp/abc123") return result.stdout.trim(); } @@ -984,13 +981,10 @@ export class StreamManager extends EventEmitter { // Clean up stream temp directory using runtime if (streamInfo.runtimeTempDir) { try { - const result = await streamInfo.runtime.exec( - `rm -rf "${streamInfo.runtimeTempDir}"`, - { - cwd: "~", - timeout: 10, - } - ); + const result = await streamInfo.runtime.exec(`rm -rf "${streamInfo.runtimeTempDir}"`, { + cwd: "~", + timeout: 10, + }); await result.exitCode; // Wait for completion log.debug(`Cleaned up temp dir: ${streamInfo.runtimeTempDir}`); } catch (error) { From 3adebdcf0e76b7e0feed1aa8cb0cdfa0a0ed5a7a Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 27 Oct 2025 01:38:49 +0000 Subject: [PATCH 16/17] Fix tempDir references added in main branch --- src/services/tools/bash.test.ts | 2 +- src/services/tools/file_edit_operation.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index 89c44ad3b..90c401bfb 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -1251,7 +1251,7 @@ describe("SSH runtime redundant cd detection", () => { const tool = createBashTool({ cwd, runtime: sshRuntime, - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, }); return { diff --git a/src/services/tools/file_edit_operation.test.ts b/src/services/tools/file_edit_operation.test.ts index c8510aa6f..ec28e839c 100644 --- a/src/services/tools/file_edit_operation.test.ts +++ b/src/services/tools/file_edit_operation.test.ts @@ -66,7 +66,7 @@ describe("executeFileEditOperation", () => { config: { cwd: testCwd, runtime: mockRuntime, - tempDir: "/tmp", + runtimeTempDir: "/tmp", }, filePath: testFilePath, operation: () => ({ success: true, newContent: "test", metadata: {} }), From 2eda504ea6d0ca1d34d0ea685de1c53f612305d1 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 27 Oct 2025 01:50:01 +0000 Subject: [PATCH 17/17] =?UTF-8?q?=F0=9F=A4=96=20Rerun=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit