diff --git a/src/services/aiService.ts b/src/services/aiService.ts index fe15e1ca23..cdf89d0a35 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); @@ -523,14 +523,14 @@ 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 +695,7 @@ export class AIService extends EventEmitter { modelString, historySequence, systemMessage, + runtime, abortSignal, tools, { diff --git a/src/services/ipcMain.ts b/src/services/ipcMain.ts index d32fc4f2ed..16bf340b51 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/streamManager.test.ts b/src/services/streamManager.test.ts index 18342d4938..0ceb575a7f 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; @@ -38,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(); @@ -85,6 +87,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 1, "You are a helpful assistant", + runtime, undefined, {} ); @@ -102,6 +105,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 2, "You are a helpful assistant", + runtime, undefined, {} ); @@ -273,6 +277,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 1, "system", + runtime, undefined, {} ), @@ -283,6 +288,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 2, "system", + runtime, undefined, {} ), @@ -293,6 +299,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => { "anthropic:claude-sonnet-4-5", 3, "system", + runtime, undefined, {} ), diff --git a/src/services/streamManager.ts b/src/services/streamManager.ts index fc9a656a92..5a05b9af6c 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, @@ -31,6 +28,8 @@ 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"; +import { execBuffered } from "@/utils/runtime/helpers"; // Type definitions for stream parts with extended properties interface ReasoningDeltaPart { @@ -107,7 +106,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,12 +243,26 @@ 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 }); - return tempDir; + 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`; + const result = await execBuffered(runtime, command, { + 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(); } /** @@ -429,7 +444,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 +524,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 +978,17 @@ 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 +1111,7 @@ export class StreamManager extends EventEmitter { modelString: string, historySequence: number, system: string, + runtime: Runtime, abortSignal?: AbortSignal, tools?: Record, initialMetadata?: Partial, @@ -1123,18 +1145,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 0542280b2a..90c401bfb8 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -22,7 +22,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, }); @@ -164,7 +164,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", }); @@ -203,7 +203,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", }); @@ -235,7 +235,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", }); @@ -271,7 +271,7 @@ describe("bash tool", () => { const tool = createBashTool({ cwd: process.cwd(), runtime: createRuntime({ type: "local", srcBaseDir: "/tmp" }), - tempDir: tempDir.path, + runtimeTempDir: tempDir.path, // overflow_policy not specified - should default to tmpfile }); @@ -289,7 +289,8 @@ describe("bash tool", () => { expect(result.error).toContain("saved to"); expect(result.error).not.toContain("[OUTPUT TRUNCATED"); - // Verify temp file was created + // 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); @@ -303,7 +304,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) @@ -355,7 +356,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) @@ -398,7 +399,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 @@ -440,7 +441,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 @@ -480,7 +481,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) @@ -510,7 +511,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) @@ -1250,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/bash.ts b/src/services/tools/bash.ts index 8def72db6e..00618fa858 100644 --- a/src/services/tools/bash.ts +++ b/src/services/tools/bash.ts @@ -419,7 +419,10 @@ 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 runtime temp directory (managed by StreamManager) + // 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 diff --git a/src/services/tools/file_edit_insert.test.ts b/src/services/tools/file_edit_insert.test.ts index b9353e80b1..562f50be11 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 ffb90a1afe..ec28e839c9 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", }; } @@ -66,7 +66,7 @@ describe("executeFileEditOperation", () => { config: { cwd: testCwd, runtime: mockRuntime, - tempDir: "/tmp", + runtimeTempDir: "/tmp", }, filePath: testFilePath, operation: () => ({ success: true, newContent: "test", metadata: {} }), diff --git a/src/services/tools/file_edit_replace.test.ts b/src/services/tools/file_edit_replace.test.ts index 0082028b4f..6f3d62217b 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 69a28fe69d..5bd855ade2 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 f343d578d4..206d13a725 100644 --- a/src/services/tools/todo.test.ts +++ b/src/services/tools/todo.test.ts @@ -6,16 +6,16 @@ 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 - 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([]); }); }); }); diff --git a/src/services/tools/todo.ts b/src/services/tools/todo.ts index aedb96cf96..8794c46cea 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, }; diff --git a/src/utils/tools/tools.ts b/src/utils/tools/tools.ts index 952abebef1..a876a17a8c 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"; }