Skip to content

Commit 52adb75

Browse files
authored
🤖 Pass abort controllers through runtime stack (#456)
Prevents indefinite hangs when cancelling long-running operations by passing abort signals through the runtime stack. ## Changes - **Runtime interfaces**: Added optional `abortSignal` parameter to operations with long timeouts (≥30s) - **LocalRuntime**: Accepts but ignores abort signals (local operations are fast) - **SSHRuntime**: Passes abort signals to all `exec()` calls and spawn() operations - **Tool integration**: Bash tool passes abort signal to runtime operations ## Impact Long-running operations can now be cancelled cleanly: - Git operations (clone, checkout, init hooks) - SSH file I/O (readFile, writeFile) - Workspace operations (create, rename, delete) - Bundle creation for SSH sync _Generated with `cmux`_
1 parent 817a2f2 commit 52adb75

File tree

11 files changed

+151
-45
lines changed

11 files changed

+151
-45
lines changed

src/runtime/LocalRuntime.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ export class LocalRuntime implements Runtime {
209209
return { stdout, stderr, stdin, exitCode, duration };
210210
}
211211

212-
readFile(filePath: string): ReadableStream<Uint8Array> {
212+
readFile(filePath: string, _abortSignal?: AbortSignal): ReadableStream<Uint8Array> {
213+
// Note: _abortSignal ignored for local operations (fast, no need for cancellation)
213214
const nodeStream = fs.createReadStream(filePath);
214215

215216
// Handle errors by wrapping in a transform
@@ -238,7 +239,8 @@ export class LocalRuntime implements Runtime {
238239
});
239240
}
240241

241-
writeFile(filePath: string): WritableStream<Uint8Array> {
242+
writeFile(filePath: string, _abortSignal?: AbortSignal): WritableStream<Uint8Array> {
243+
// Note: _abortSignal ignored for local operations (fast, no need for cancellation)
242244
let tempPath: string;
243245
let writer: WritableStreamDefaultWriter<Uint8Array>;
244246
let resolvedPath: string;
@@ -304,7 +306,8 @@ export class LocalRuntime implements Runtime {
304306
});
305307
}
306308

307-
async stat(filePath: string): Promise<FileStat> {
309+
async stat(filePath: string, _abortSignal?: AbortSignal): Promise<FileStat> {
310+
// Note: _abortSignal ignored for local operations (fast, no need for cancellation)
308311
try {
309312
const stats = await fsPromises.stat(filePath);
310313
return {
@@ -480,10 +483,12 @@ export class LocalRuntime implements Runtime {
480483
async renameWorkspace(
481484
projectPath: string,
482485
oldName: string,
483-
newName: string
486+
newName: string,
487+
_abortSignal?: AbortSignal
484488
): Promise<
485489
{ success: true; oldPath: string; newPath: string } | { success: false; error: string }
486490
> {
491+
// Note: _abortSignal ignored for local operations (fast, no need for cancellation)
487492
// Compute workspace paths using canonical method
488493
const oldPath = this.getWorkspacePath(projectPath, oldName);
489494
const newPath = this.getWorkspacePath(projectPath, newName);
@@ -503,8 +508,10 @@ export class LocalRuntime implements Runtime {
503508
async deleteWorkspace(
504509
projectPath: string,
505510
workspaceName: string,
506-
force: boolean
511+
force: boolean,
512+
_abortSignal?: AbortSignal
507513
): Promise<{ success: true; deletedPath: string } | { success: false; error: string }> {
514+
// Note: _abortSignal ignored for local operations (fast, no need for cancellation)
508515
// Compute workspace path using the canonical method
509516
const deletedPath = this.getWorkspacePath(projectPath, workspaceName);
510517

src/runtime/Runtime.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ export interface WorkspaceCreationParams {
119119
directoryName: string;
120120
/** Logger for streaming creation progress and init hook output */
121121
initLogger: InitLogger;
122+
/** Optional abort signal for cancellation */
123+
abortSignal?: AbortSignal;
122124
}
123125

124126
/**
@@ -145,6 +147,8 @@ export interface WorkspaceInitParams {
145147
workspacePath: string;
146148
/** Logger for streaming initialization progress and output */
147149
initLogger: InitLogger;
150+
/** Optional abort signal for cancellation */
151+
abortSignal?: AbortSignal;
148152
}
149153

150154
/**
@@ -208,26 +212,29 @@ export interface Runtime {
208212
/**
209213
* Read file contents as a stream
210214
* @param path Absolute or relative path to file
215+
* @param abortSignal Optional abort signal for cancellation
211216
* @returns Readable stream of file contents
212217
* @throws RuntimeError if file cannot be read
213218
*/
214-
readFile(path: string): ReadableStream<Uint8Array>;
219+
readFile(path: string, abortSignal?: AbortSignal): ReadableStream<Uint8Array>;
215220

216221
/**
217222
* Write file contents atomically from a stream
218223
* @param path Absolute or relative path to file
224+
* @param abortSignal Optional abort signal for cancellation
219225
* @returns Writable stream for file contents
220226
* @throws RuntimeError if file cannot be written
221227
*/
222-
writeFile(path: string): WritableStream<Uint8Array>;
228+
writeFile(path: string, abortSignal?: AbortSignal): WritableStream<Uint8Array>;
223229

224230
/**
225231
* Get file statistics
226232
* @param path Absolute or relative path to file/directory
233+
* @param abortSignal Optional abort signal for cancellation
227234
* @returns File statistics
228235
* @throws RuntimeError if path does not exist or cannot be accessed
229236
*/
230-
stat(path: string): Promise<FileStat>;
237+
stat(path: string, abortSignal?: AbortSignal): Promise<FileStat>;
231238

232239
/**
233240
* Resolve a path to its absolute, canonical form (expanding tildes, resolving symlinks, etc.).
@@ -310,12 +317,14 @@ export interface Runtime {
310317
* @param projectPath Project root path (local path, used for git commands in LocalRuntime and to extract project name)
311318
* @param oldName Current workspace name
312319
* @param newName New workspace name
320+
* @param abortSignal Optional abort signal for cancellation
313321
* @returns Promise resolving to Result with old/new paths on success, or error message
314322
*/
315323
renameWorkspace(
316324
projectPath: string,
317325
oldName: string,
318-
newName: string
326+
newName: string,
327+
abortSignal?: AbortSignal
319328
): Promise<
320329
{ success: true; oldPath: string; newPath: string } | { success: false; error: string }
321330
>;
@@ -333,12 +342,14 @@ export interface Runtime {
333342
* @param projectPath Project root path (local path, used for git commands in LocalRuntime and to extract project name)
334343
* @param workspaceName Workspace name to delete
335344
* @param force If true, force deletion even with uncommitted changes or special conditions (submodules, etc.)
345+
* @param abortSignal Optional abort signal for cancellation
336346
* @returns Promise resolving to Result with deleted path on success, or error message
337347
*/
338348
deleteWorkspace(
339349
projectPath: string,
340350
workspaceName: string,
341-
force: boolean
351+
force: boolean,
352+
abortSignal?: AbortSignal
342353
): Promise<{ success: true; deletedPath: string } | { success: false; error: string }>;
343354

344355
/**

0 commit comments

Comments
 (0)