@mastra/blaxel 0.0.2 → 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # @mastra/blaxel
2
2
 
3
+ ## 0.1.0-alpha.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Abort signal support in sandbox commands ([#13597](https://github.com/mastra-ai/mastra/pull/13597))
8
+ - Sandbox commands can now be cancelled via `abortSignal` in command options
9
+ - Partial stdout/stderr output is now preserved when a command is aborted or times out
10
+
11
+ - Added background process management support for Blaxel sandboxes. Agents can now spawn, monitor, and kill long-running processes using the standard `ProcessHandle` interface. ([`79177b1`](https://github.com/mastra-ai/mastra/commit/79177b1fa8c9221196290d38f6ed5e4c592dc4e2))
12
+
13
+ **Example usage:**
14
+
15
+ ```typescript
16
+ const sandbox = new BlaxelSandbox({ timeout: '5m' });
17
+ const workspace = new Workspace({ sandbox });
18
+
19
+ // Process manager is available via sandbox.processes
20
+ const handle = await sandbox.processes.spawn('python server.py');
21
+
22
+ // Monitor output
23
+ handle.onStdout(data => console.log(data));
24
+
25
+ // Check status
26
+ const info = await sandbox.processes.list();
27
+
28
+ // Kill when done
29
+ await handle.kill();
30
+ ```
31
+
32
+ **Note:** Process stdin is not supported in Blaxel sandboxes.
33
+
34
+ **Additional improvements:**
35
+ - Fixed detection of expired sandboxes, ensuring operations automatically retry when a sandbox has timed out
36
+
37
+ ### Patch Changes
38
+
39
+ - Fixed command timeouts in Blaxel sandboxes so long-running commands now respect configured limits. ([#13520](https://github.com/mastra-ai/mastra/pull/13520))
40
+
41
+ Changed the default Blaxel image to `blaxel/ts-app:latest` (Debian-based), which supports both S3 and GCS mounts out of the box.
42
+
43
+ Added distro detection for mount scripts so S3 mounts work on Alpine-based images (e.g. `blaxel/node:latest`) via `apk`, and GCS mounts give a clear error on Alpine since gcsfuse is unavailable.
44
+
45
+ Removed working directory from sandbox instructions to avoid breaking prompt caching.
46
+
47
+ - Remove internal `processes` field from sandbox provider options ([#13597](https://github.com/mastra-ai/mastra/pull/13597))
48
+
49
+ The `processes` field is no longer exposed in constructor options for E2B, Daytona, and Blaxel sandbox providers. This field is managed internally and was not intended to be user-configurable.
50
+
51
+ - Updated dependencies [[`504fc8b`](https://github.com/mastra-ai/mastra/commit/504fc8b9d0ddab717577ad3bf9c95ea4bd5377bd), [`f9c150b`](https://github.com/mastra-ai/mastra/commit/f9c150b7595ad05ad9cc9a11098e2944361e8c22), [`88de7e8`](https://github.com/mastra-ai/mastra/commit/88de7e8dfe4b7e1951a9e441bb33136e705ce24e), [`edee4b3`](https://github.com/mastra-ai/mastra/commit/edee4b37dff0af515fc7cc0e8d71ee39e6a762f0), [`3790c75`](https://github.com/mastra-ai/mastra/commit/3790c7578cc6a47d854eb12d89e6b1912867fe29), [`e7a235b`](https://github.com/mastra-ai/mastra/commit/e7a235be6472e0c870ed6c791ddb17c492dc188b), [`d51d298`](https://github.com/mastra-ai/mastra/commit/d51d298953967aab1f58ec965b644d109214f085), [`6dbeeb9`](https://github.com/mastra-ai/mastra/commit/6dbeeb94a8b1eebb727300d1a98961f882180794), [`d5f0d8d`](https://github.com/mastra-ai/mastra/commit/d5f0d8d6a03e515ddaa9b5da19b7e44b8357b07b), [`09c3b18`](https://github.com/mastra-ai/mastra/commit/09c3b1802ff14e243a8a8baea327440bc8cc2e32), [`b896379`](https://github.com/mastra-ai/mastra/commit/b8963791c6afa79484645fcec596a201f936b9a2), [`85c84eb`](https://github.com/mastra-ai/mastra/commit/85c84ebb78aebfcba9d209c8e152b16d7a00cb71), [`a89272a`](https://github.com/mastra-ai/mastra/commit/a89272a5d71939b9fcd284e6a6dc1dd091a6bdcf), [`ee9c8df`](https://github.com/mastra-ai/mastra/commit/ee9c8df644f19d055af5f496bf4942705f5a47b7), [`77b4a25`](https://github.com/mastra-ai/mastra/commit/77b4a254e51907f8ff3a3ba95596a18e93ae4b35), [`276246e`](https://github.com/mastra-ai/mastra/commit/276246e0b9066a1ea48bbc70df84dbe528daaf99), [`08ecfdb`](https://github.com/mastra-ai/mastra/commit/08ecfdbdad6fb8285deef86a034bdf4a6047cfca), [`d5f628c`](https://github.com/mastra-ai/mastra/commit/d5f628ca86c6f6f3ff1035d52f635df32dd81cab), [`524c0f3`](https://github.com/mastra-ai/mastra/commit/524c0f3c434c3d9d18f66338dcef383d6161b59c), [`c18a0e9`](https://github.com/mastra-ai/mastra/commit/c18a0e9cef1e4ca004b2963d35e4cfc031971eac), [`4bd21ea`](https://github.com/mastra-ai/mastra/commit/4bd21ea43d44d0a0427414fc047577f9f0aa3bec), [`115a7a4`](https://github.com/mastra-ai/mastra/commit/115a7a47db5e9896fec12ae6507501adb9ec89bf), [`22a48ae`](https://github.com/mastra-ai/mastra/commit/22a48ae2513eb54d8d79dad361fddbca97a155e8), [`3c6ef79`](https://github.com/mastra-ai/mastra/commit/3c6ef798481e00d6d22563be2de98818fd4dd5e0), [`9311c17`](https://github.com/mastra-ai/mastra/commit/9311c17d7a0640d9c4da2e71b814dc67c57c6369), [`7edf78f`](https://github.com/mastra-ai/mastra/commit/7edf78f80422c43e84585f08ba11df0d4d0b73c5), [`1c4221c`](https://github.com/mastra-ai/mastra/commit/1c4221cf6032ec98d0e094d4ee11da3e48490d96), [`d25b9ea`](https://github.com/mastra-ai/mastra/commit/d25b9eabd400167255a97b690ffbc4ee4097ded5), [`fe1ce5c`](https://github.com/mastra-ai/mastra/commit/fe1ce5c9211c03d561606fda95cbfe7df1d9a9b5), [`b03c0e0`](https://github.com/mastra-ai/mastra/commit/b03c0e0389a799523929a458b0509c9e4244d562), [`0a8366b`](https://github.com/mastra-ai/mastra/commit/0a8366b0a692fcdde56c4d526e4cf03c502ae4ac), [`85664e9`](https://github.com/mastra-ai/mastra/commit/85664e9fd857320fbc245e301f764f45f66f32a3), [`bc79650`](https://github.com/mastra-ai/mastra/commit/bc796500c6e0334faa158a96077e3fb332274869), [`9257d01`](https://github.com/mastra-ai/mastra/commit/9257d01d1366d81f84c582fe02b5e200cf9621f4), [`3a3a59e`](https://github.com/mastra-ai/mastra/commit/3a3a59e8ffaa6a985fe3d9a126a3f5ade11a6724), [`3108d4e`](https://github.com/mastra-ai/mastra/commit/3108d4e649c9fddbf03253a6feeb388a5fa9fa5a), [`0c33b2c`](https://github.com/mastra-ai/mastra/commit/0c33b2c9db537f815e1c59e2c898ffce2e395a79), [`191e5bd`](https://github.com/mastra-ai/mastra/commit/191e5bd29b82f5bda35243945790da7bc7b695c2), [`f77cd94`](https://github.com/mastra-ai/mastra/commit/f77cd94c44eabed490384e7d19232a865e13214c), [`e8135c7`](https://github.com/mastra-ai/mastra/commit/e8135c7e300dac5040670eec7eab896ac6092e30), [`daca48f`](https://github.com/mastra-ai/mastra/commit/daca48f0fb17b7ae0b62a2ac40cf0e491b2fd0b7), [`257d14f`](https://github.com/mastra-ai/mastra/commit/257d14faca5931f2e4186fc165b6f0b1f915deee), [`352f25d`](https://github.com/mastra-ai/mastra/commit/352f25da316b24cdd5b410fd8dddf6a8b763da2a), [`93477d0`](https://github.com/mastra-ai/mastra/commit/93477d0769b8a13ea5ed73d508d967fb23eaeed9), [`31c78b3`](https://github.com/mastra-ai/mastra/commit/31c78b3eb28f58a8017f1dcc795c33214d87feac), [`0bc0720`](https://github.com/mastra-ai/mastra/commit/0bc07201095791858087cc56f353fcd65e87ab54), [`36516ac`](https://github.com/mastra-ai/mastra/commit/36516aca1021cbeb42e74751b46a2614101f37c8), [`e947652`](https://github.com/mastra-ai/mastra/commit/e9476527fdecb4449e54570e80dfaf8466901254), [`3c6ef79`](https://github.com/mastra-ai/mastra/commit/3c6ef798481e00d6d22563be2de98818fd4dd5e0), [`9257d01`](https://github.com/mastra-ai/mastra/commit/9257d01d1366d81f84c582fe02b5e200cf9621f4), [`ec248f6`](https://github.com/mastra-ai/mastra/commit/ec248f6b56e8a037c066c49b2178e2507471d988)]:
52
+ - @mastra/core@1.9.0-alpha.0
53
+
3
54
  ## 0.0.2
4
55
 
5
56
  ### Patch Changes
package/LICENSE.md CHANGED
@@ -1,3 +1,18 @@
1
+ Portions of this software are licensed as follows:
2
+
3
+ - All content that resides under any directory named "ee/" within this
4
+ repository, including but not limited to:
5
+ - `packages/core/src/auth/ee/`
6
+ - `packages/server/src/server/auth/ee/`
7
+ is licensed under the license defined in `ee/LICENSE`.
8
+
9
+ - All third-party components incorporated into the Mastra Software are
10
+ licensed under the original license provided by the owner of the
11
+ applicable component.
12
+
13
+ - Content outside of the above-mentioned directories or restrictions is
14
+ available under the "Apache License 2.0" as defined below.
15
+
1
16
  # Apache License 2.0
2
17
 
3
18
  Copyright (c) 2025 Kepler Software, Inc.
package/README.md CHANGED
@@ -34,7 +34,7 @@ const agent = new Agent({
34
34
  | Option | Type | Default | Description |
35
35
  | ---------- | ------------------------------------- | ---------------------------- | ------------------------------------------------------ |
36
36
  | `id` | `string` | auto-generated | Unique identifier for the sandbox instance |
37
- | `image` | `string` | `'blaxel/py-app:latest'` | Docker image to use |
37
+ | `image` | `string` | `'blaxel/ts-app:latest'` | Docker image to use |
38
38
  | `memory` | `number` | `4096` | Memory allocation in MB |
39
39
  | `timeout` | `string` | `'5m'` | Sandbox TTL as a duration string (e.g. `'5m'`, `'1h'`) |
40
40
  | `env` | `Record<string, string>` | — | Environment variables to set in the sandbox |
package/dist/index.cjs CHANGED
@@ -37,6 +37,16 @@ function validateEndpoint(endpoint) {
37
37
  throw new Error(`Invalid endpoint URL scheme: "${parsed.protocol}". Only http: and https: are allowed.`);
38
38
  }
39
39
  }
40
+ async function detectPackageManager(sandbox) {
41
+ const result = await runCommand(
42
+ sandbox,
43
+ 'which apt-get >/dev/null 2>&1 && echo "apt" || (which apk >/dev/null 2>&1 && echo "apk" || echo "unknown")'
44
+ );
45
+ const pm = result.stdout.trim();
46
+ if (pm === "apt") return "apt";
47
+ if (pm === "apk") return "apk";
48
+ return "unknown";
49
+ }
40
50
  async function runCommand(sandbox, command, options) {
41
51
  const result = await sandbox.process.exec({
42
52
  command,
@@ -59,25 +69,45 @@ async function mountS3(mountPath, config, ctx) {
59
69
  if (checkResult.stdout.includes("not found")) {
60
70
  logger.warn(`${LOG_PREFIX} s3fs not found, attempting runtime installation...`);
61
71
  logger.info(`${LOG_PREFIX} Tip: For faster startup, pre-install s3fs in your sandbox image`);
62
- const updateResult = await runCommand(sandbox, "apt-get update 2>&1", { timeout: 6e4 });
63
- if (updateResult.exitCode !== 0) {
64
- throw new Error(
65
- `Failed to update package lists for s3fs installation.
72
+ const pm = await detectPackageManager(sandbox);
73
+ logger.debug(`${LOG_PREFIX} Detected package manager: ${pm}`);
74
+ if (pm === "apt") {
75
+ const updateResult = await runCommand(sandbox, "apt-get update 2>&1", { timeout: 6e4 });
76
+ if (updateResult.exitCode !== 0) {
77
+ throw new Error(
78
+ `Failed to update package lists for s3fs installation.
66
79
  Error details: ${updateResult.stderr || updateResult.stdout}`
80
+ );
81
+ }
82
+ const installResult = await runCommand(
83
+ sandbox,
84
+ "apt-get install -y s3fs fuse 2>&1 || apt-get install -y s3fs-fuse fuse 2>&1",
85
+ { timeout: 12e4 }
67
86
  );
68
- }
69
- const installResult = await runCommand(
70
- sandbox,
71
- "apt-get install -y s3fs fuse 2>&1 || apt-get install -y s3fs-fuse fuse 2>&1",
72
- { timeout: 12e4 }
73
- );
74
- if (installResult.exitCode !== 0) {
75
- throw new Error(
76
- `Failed to install s3fs. For S3 mounting, your sandbox image needs s3fs and fuse packages.
87
+ if (installResult.exitCode !== 0) {
88
+ throw new Error(
89
+ `Failed to install s3fs. For S3 mounting, your sandbox image needs s3fs and fuse packages.
77
90
 
78
91
  Pre-install in your image: apt-get install -y s3fs fuse
79
92
 
80
93
  Error details: ${installResult.stderr || installResult.stdout}`
94
+ );
95
+ }
96
+ } else if (pm === "apk") {
97
+ const installResult = await runCommand(sandbox, "apk add --no-cache s3fs-fuse fuse 2>&1", { timeout: 12e4 });
98
+ if (installResult.exitCode !== 0) {
99
+ throw new Error(
100
+ `Failed to install s3fs on Alpine Linux. Ensure the Alpine community repository is enabled.
101
+
102
+ Pre-install in your image: apk add --no-cache s3fs-fuse fuse
103
+
104
+ Error details: ${installResult.stderr || installResult.stdout}`
105
+ );
106
+ }
107
+ } else {
108
+ throw new Error(
109
+ `Cannot install s3fs: no supported package manager found (need apt-get or apk).
110
+ Use a Debian-based image (e.g. blaxel/ts-app:latest) or Alpine-based image (e.g. blaxel/node:latest), or pre-install s3fs in your custom image.`
81
111
  );
82
112
  }
83
113
  }
@@ -139,6 +169,26 @@ async function mountGCS(mountPath, config, ctx) {
139
169
  const checkResult = await runCommand(sandbox, 'which gcsfuse || echo "not found"');
140
170
  if (checkResult.stdout.includes("not found")) {
141
171
  logger.warn(`${LOG_PREFIX} gcsfuse not found, attempting runtime installation...`);
172
+ const pm = await detectPackageManager(sandbox);
173
+ logger.debug(`${LOG_PREFIX} Detected package manager: ${pm}`);
174
+ if (pm === "apk") {
175
+ throw new Error(
176
+ `gcsfuse is not available on Alpine Linux. Google only provides gcsfuse packages for Debian/Ubuntu.
177
+
178
+ Use a Debian-based Blaxel image for GCS mounts:
179
+ new BlaxelSandbox({ image: 'blaxel/ts-app:latest' })
180
+ new BlaxelSandbox({ image: 'blaxel/py-app:latest' })`
181
+ );
182
+ }
183
+ if (pm !== "apt") {
184
+ throw new Error(
185
+ `Cannot install gcsfuse: no supported package manager found (need apt-get).
186
+ gcsfuse is only available on Debian/Ubuntu-based images.
187
+
188
+ Use a Debian-based Blaxel image:
189
+ new BlaxelSandbox({ image: 'blaxel/ts-app:latest' })`
190
+ );
191
+ }
142
192
  logger.info(`${LOG_PREFIX} Tip: For faster startup, pre-install gcsfuse in your sandbox image`);
143
193
  const codenameResult = await runCommand(
144
194
  sandbox,
@@ -209,6 +259,138 @@ ${installResult.stderr}`
209
259
  throw new Error(`Failed to mount GCS bucket: ${result.stderr || result.stdout}`);
210
260
  }
211
261
  }
262
+ var BlaxelProcessHandle = class extends workspace.ProcessHandle {
263
+ pid;
264
+ _identifier;
265
+ _sandbox;
266
+ _startTime;
267
+ _exitCode;
268
+ _waitPromise = null;
269
+ _streamingDone = null;
270
+ _closeStream = null;
271
+ _killed = false;
272
+ constructor(pid, identifier, sandbox, startTime, options) {
273
+ super(options);
274
+ this.pid = pid;
275
+ this._identifier = identifier;
276
+ this._sandbox = sandbox;
277
+ this._startTime = startTime;
278
+ }
279
+ get exitCode() {
280
+ return this._exitCode;
281
+ }
282
+ /** @internal Set by the process manager after streaming starts. */
283
+ set streamControl(control) {
284
+ this._closeStream = control.close;
285
+ this._streamingDone = control.wait();
286
+ this._streamingDone.then(() => this._resolveExitCode()).catch(() => this._resolveExitCode());
287
+ }
288
+ /** Fetch exit code from Blaxel and set _exitCode. No-op if already set. */
289
+ async _resolveExitCode() {
290
+ if (this._exitCode !== void 0) return;
291
+ try {
292
+ const proc = await this._sandbox.process.get(this._identifier);
293
+ this._exitCode = proc.status === "completed" ? proc.exitCode ?? 0 : proc.exitCode ?? 1;
294
+ } catch {
295
+ if (this._exitCode === void 0) {
296
+ this._exitCode = 1;
297
+ }
298
+ }
299
+ }
300
+ async wait() {
301
+ if (!this._waitPromise) {
302
+ this._waitPromise = this._doWait();
303
+ }
304
+ return this._waitPromise;
305
+ }
306
+ async _doWait() {
307
+ if (this._streamingDone) {
308
+ await this._streamingDone.catch(() => {
309
+ });
310
+ }
311
+ if (this._killed) {
312
+ return {
313
+ success: false,
314
+ exitCode: this._exitCode ?? 137,
315
+ stdout: this.stdout,
316
+ stderr: this.stderr,
317
+ executionTimeMs: Date.now() - this._startTime
318
+ };
319
+ }
320
+ await this._resolveExitCode();
321
+ return {
322
+ success: this._exitCode === 0,
323
+ exitCode: this._exitCode ?? 1,
324
+ stdout: this.stdout,
325
+ stderr: this.stderr,
326
+ executionTimeMs: Date.now() - this._startTime
327
+ };
328
+ }
329
+ async kill() {
330
+ if (this._exitCode !== void 0) return false;
331
+ this._killed = true;
332
+ this._exitCode = 137;
333
+ this._closeStream?.();
334
+ try {
335
+ await this._sandbox.process.kill(this._identifier);
336
+ } catch {
337
+ }
338
+ return true;
339
+ }
340
+ async sendStdin(_data) {
341
+ throw new Error("Blaxel sandboxes do not support stdin");
342
+ }
343
+ };
344
+ var BlaxelProcessManager = class extends workspace.SandboxProcessManager {
345
+ constructor(opts = {}) {
346
+ super({ env: opts.env });
347
+ }
348
+ async spawn(command, options = {}) {
349
+ return this.sandbox.retryOnDead(async () => {
350
+ const blaxel = this.sandbox.instance;
351
+ const mergedEnv = { ...this.env, ...options.env };
352
+ const envs = Object.fromEntries(
353
+ Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
354
+ );
355
+ const result = await blaxel.process.exec({
356
+ command,
357
+ waitForCompletion: false,
358
+ workingDir: options.cwd,
359
+ ...Object.keys(envs).length > 0 && { env: envs },
360
+ ...options.timeout && { timeout: Math.ceil(options.timeout / 1e3) }
361
+ });
362
+ const identifier = result.pid;
363
+ const pid = parseInt(identifier, 10);
364
+ const handle = new BlaxelProcessHandle(pid, identifier, blaxel, Date.now(), options);
365
+ const streamControl = blaxel.process.streamLogs(identifier, {
366
+ onStdout: (data) => handle.emitStdout(data),
367
+ onStderr: (data) => handle.emitStderr(data),
368
+ onError: (err) => {
369
+ const msg = err instanceof Error ? err.message : String(err);
370
+ handle.emitStderr(msg);
371
+ }
372
+ });
373
+ handle.streamControl = streamControl;
374
+ this._tracked.set(pid, handle);
375
+ return handle;
376
+ });
377
+ }
378
+ async list() {
379
+ const result = [];
380
+ for (const [pid, handle] of this._tracked) {
381
+ result.push({
382
+ pid,
383
+ command: handle.command,
384
+ running: handle.exitCode === void 0,
385
+ exitCode: handle.exitCode
386
+ });
387
+ }
388
+ return result;
389
+ }
390
+ async get(pid) {
391
+ return this._tracked.get(pid);
392
+ }
393
+ };
212
394
 
213
395
  // src/sandbox/index.ts
214
396
  var SAFE_MOUNT_PATH = /^\/[a-zA-Z0-9_.\-/]+$/;
@@ -250,9 +432,13 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
250
432
  ports;
251
433
  // Non-optional (initialized by BaseSandbox)
252
434
  constructor(options = {}) {
253
- super({ ...options, name: "BlaxelSandbox" });
435
+ super({
436
+ ...options,
437
+ name: "BlaxelSandbox",
438
+ processes: new BlaxelProcessManager({ env: options.env })
439
+ });
254
440
  this.id = options.id ?? this.generateId();
255
- this.image = options.image ?? "blaxel/py-app:latest";
441
+ this.image = options.image ?? "blaxel/ts-app:latest";
256
442
  this.memory = options.memory ?? 4096;
257
443
  this.timeout = options.timeout;
258
444
  this.env = options.env ?? {};
@@ -595,7 +781,6 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
595
781
  image: this.image,
596
782
  memory: this.memory,
597
783
  ...this.timeout && { ttl: this.timeout },
598
- envs: Object.entries(this.env).map(([name, value]) => ({ name, value })),
599
784
  labels: {
600
785
  ...this.labels,
601
786
  "mastra-sandbox-id": this.id
@@ -653,6 +838,13 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
653
838
  * Status management is handled by the base class.
654
839
  */
655
840
  async stop() {
841
+ if (this.processes) {
842
+ try {
843
+ const procs = await this.processes.list();
844
+ await Promise.all(procs.filter((p) => p.running).map((p) => this.processes.kill(p.pid)));
845
+ } catch {
846
+ }
847
+ }
656
848
  for (const mountPath of [...this.mounts.entries.keys()]) {
657
849
  try {
658
850
  await this.unmount(mountPath);
@@ -667,6 +859,13 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
667
859
  * Status management is handled by the base class.
668
860
  */
669
861
  async destroy() {
862
+ if (this.processes) {
863
+ try {
864
+ const procs = await this.processes.list();
865
+ await Promise.all(procs.filter((p) => p.running).map((p) => this.processes.kill(p.pid)));
866
+ } catch {
867
+ }
868
+ }
670
869
  for (const mountPath of [...this.mounts.entries.keys()]) {
671
870
  try {
672
871
  await this.unmount(mountPath);
@@ -717,7 +916,7 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
717
916
  getInstructions() {
718
917
  const mountCount = this.mounts.entries.size;
719
918
  const mountInfo = mountCount > 0 ? ` ${mountCount} filesystem(s) mounted via FUSE.` : "";
720
- return `Cloud sandbox with /home/user as working directory.${mountInfo}`;
919
+ return `Cloud sandbox.${mountInfo}`;
721
920
  }
722
921
  // ---------------------------------------------------------------------------
723
922
  // Internal Helpers
@@ -737,8 +936,8 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
737
936
  */
738
937
  isSandboxDeadError(error) {
739
938
  if (!error) return false;
740
- const errorStr = errorToString(error);
741
- return errorStr.includes("TERMINATED") || errorStr.includes("sandbox was not found") || errorStr.includes("Sandbox not found");
939
+ const errorStr = errorToString(error).toLowerCase();
940
+ return errorStr.includes("terminated") || errorStr.includes("sandbox was not found") || errorStr.includes("sandbox not found") || errorStr.includes('"not found"');
742
941
  }
743
942
  /**
744
943
  * Handle sandbox timeout by clearing the instance and resetting state.
@@ -756,6 +955,31 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
756
955
  }
757
956
  this.status = "stopped";
758
957
  }
958
+ /**
959
+ * Execute an operation with automatic retry if the sandbox is found to be dead.
960
+ *
961
+ * When the Blaxel sandbox times out or crashes mid-operation, this method
962
+ * resets sandbox state, restarts it, and retries the operation once.
963
+ *
964
+ * @internal Used by BlaxelProcessManager to handle dead sandboxes during spawn.
965
+ */
966
+ async retryOnDead(fn) {
967
+ try {
968
+ return await fn();
969
+ } catch (error) {
970
+ if (this.isSandboxDeadError(error) && !this._isRetrying) {
971
+ this.handleSandboxTimeout();
972
+ this._isRetrying = true;
973
+ try {
974
+ await this.ensureRunning();
975
+ return await fn();
976
+ } finally {
977
+ this._isRetrying = false;
978
+ }
979
+ }
980
+ throw error;
981
+ }
982
+ }
759
983
  // ---------------------------------------------------------------------------
760
984
  // Command Execution
761
985
  // ---------------------------------------------------------------------------
@@ -770,26 +994,77 @@ var BlaxelSandbox = class extends workspace.MastraSandbox {
770
994
  const startTime = Date.now();
771
995
  const fullCommand = args.length > 0 ? `${command} ${args.map(shellQuote).join(" ")}` : command;
772
996
  this.logger.debug(`${LOG_PREFIX} Executing: ${fullCommand}`);
997
+ let capturedStdout = "";
998
+ let capturedStderr = "";
773
999
  try {
774
1000
  const mergedEnv = { ...this.env, ...options.env };
775
1001
  const envRecord = Object.fromEntries(
776
1002
  Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
777
1003
  );
778
- const result = await sandbox.process.exec({
1004
+ const apiTimeout = options.timeout ? Math.ceil(options.timeout / 1e3) : void 0;
1005
+ const execPromise = sandbox.process.exec({
779
1006
  command: fullCommand,
780
1007
  workingDir: options.cwd,
781
1008
  env: envRecord,
782
1009
  waitForCompletion: true,
783
- ...options.timeout && { timeout: Math.ceil(options.timeout / 1e3) },
784
- ...options.onStdout || options.onStderr ? {
785
- onStdout: options.onStdout,
786
- onStderr: options.onStderr
787
- } : {}
1010
+ ...apiTimeout && { timeout: apiTimeout },
1011
+ onStdout: (data) => {
1012
+ capturedStdout += data;
1013
+ options.onStdout?.(data);
1014
+ },
1015
+ onStderr: (data) => {
1016
+ capturedStderr += data;
1017
+ options.onStderr?.(data);
1018
+ }
788
1019
  });
1020
+ const racePromises = [];
1021
+ let timer;
1022
+ let abortHandler;
1023
+ if (options.timeout) {
1024
+ racePromises.push(
1025
+ new Promise((_, reject) => {
1026
+ timer = setTimeout(() => {
1027
+ runCommand(sandbox, `pkill -f ${shellQuote(fullCommand)}`, { timeout: 5e3 }).catch(() => {
1028
+ });
1029
+ reject(new Error(`Command timed out after ${options.timeout}ms`));
1030
+ }, options.timeout);
1031
+ })
1032
+ );
1033
+ }
1034
+ if (options.abortSignal) {
1035
+ if (options.abortSignal.aborted) {
1036
+ runCommand(sandbox, `pkill -f ${shellQuote(fullCommand)}`, { timeout: 5e3 }).catch(() => {
1037
+ });
1038
+ throw new Error("Process aborted");
1039
+ }
1040
+ racePromises.push(
1041
+ new Promise((_, reject) => {
1042
+ abortHandler = () => {
1043
+ runCommand(sandbox, `pkill -f ${shellQuote(fullCommand)}`, { timeout: 5e3 }).catch(() => {
1044
+ });
1045
+ reject(new Error("Process aborted"));
1046
+ };
1047
+ options.abortSignal.addEventListener("abort", abortHandler, { once: true });
1048
+ })
1049
+ );
1050
+ }
1051
+ let result;
1052
+ try {
1053
+ if (racePromises.length > 0) {
1054
+ result = await Promise.race([execPromise, ...racePromises]);
1055
+ } else {
1056
+ result = await execPromise;
1057
+ }
1058
+ } finally {
1059
+ if (timer) clearTimeout(timer);
1060
+ if (abortHandler && options.abortSignal) {
1061
+ options.abortSignal.removeEventListener("abort", abortHandler);
1062
+ }
1063
+ }
789
1064
  const executionTimeMs = Date.now() - startTime;
790
1065
  const exitCode = result.exitCode ?? 0;
791
- const stdout = result.stdout ?? "";
792
- const stderr = result.stderr ?? "";
1066
+ const stdout = capturedStdout || result.stdout || "";
1067
+ const stderr = capturedStderr || result.stderr || "";
793
1068
  this.logger.debug(`${LOG_PREFIX} Exit code: ${exitCode} (${executionTimeMs}ms)`);
794
1069
  if (stdout) this.logger.debug(`${LOG_PREFIX} stdout:
795
1070
  ${stdout}`);
@@ -815,13 +1090,11 @@ ${stderr}`);
815
1090
  }
816
1091
  }
817
1092
  const executionTimeMs = Date.now() - startTime;
818
- const stderr = errorToString(error);
819
- this.logger.debug(`${LOG_PREFIX} Execution error (${executionTimeMs}ms): ${stderr}`);
820
1093
  return {
821
1094
  success: false,
822
1095
  exitCode: 1,
823
- stdout: "",
824
- stderr,
1096
+ stdout: capturedStdout,
1097
+ stderr: capturedStderr || errorToString(error),
825
1098
  executionTimeMs,
826
1099
  command,
827
1100
  args
@@ -830,6 +1103,7 @@ ${stderr}`);
830
1103
  }
831
1104
  };
832
1105
 
1106
+ exports.BlaxelProcessManager = BlaxelProcessManager;
833
1107
  exports.BlaxelSandbox = BlaxelSandbox;
834
1108
  //# sourceMappingURL=index.cjs.map
835
1109
  //# sourceMappingURL=index.cjs.map