@blaxel/core 0.2.87 → 0.2.88

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.
@@ -22,8 +22,8 @@ function missingCredentialsMessage() {
22
22
  return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
23
23
  }
24
24
  // Build info - these placeholders are replaced at build time by build:replace-imports
25
- const BUILD_VERSION = "0.2.87";
26
- const BUILD_COMMIT = "31085636cd9c2557198ee7399e7794e945c21687";
25
+ const BUILD_VERSION = "0.2.88";
26
+ const BUILD_COMMIT = "3d4dcbfdf0ac479adef35da9cd8523954af3e7f7";
27
27
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
28
28
  const BLAXEL_API_VERSION = "2026-04-16";
29
29
  // Cache for config.yaml tracking value
@@ -18,6 +18,23 @@ const NON_REUSABLE_SANDBOX_STATUSES = new Set([
18
18
  "DELETING",
19
19
  "DEACTIVATING",
20
20
  ]);
21
+ // Statuses that resolve on their own (a delete or deactivation in flight). The control
22
+ // plane keeps answering 409 to creates while the record is in one of these, so retrying
23
+ // instantly burns the whole attempt budget inside the window. Terminal statuses
24
+ // (FAILED, TERMINATED) accept a create immediately and are not listed here.
25
+ const TRANSIENT_SANDBOX_STATUSES = new Set([
26
+ "TERMINATING",
27
+ "DELETING",
28
+ "DEACTIVATING",
29
+ ]);
30
+ const TRANSIENT_STATUS_MAX_WAIT_MS = 30_000;
31
+ const TRANSIENT_STATUS_POLL_MS = 500;
32
+ const isSandboxNotFound = (e) => {
33
+ if (typeof e !== "object" || e === null)
34
+ return false;
35
+ const candidate = e;
36
+ return candidate.code === 404 || candidate.code === "404" || candidate.status === 404;
37
+ };
21
38
  export class SandboxInstance {
22
39
  sandbox;
23
40
  fs;
@@ -286,7 +303,9 @@ export class SandboxInstance {
286
303
  }
287
304
  static async createIfNotExists(sandbox) {
288
305
  const ATTEMPTS = 3;
306
+ let lastStatus = "unknown";
289
307
  for (let i = 0; i < ATTEMPTS; ++i) {
308
+ const finalAttempt = i === ATTEMPTS - 1;
290
309
  try {
291
310
  return await this.create(sandbox, { createIfNotExist: true });
292
311
  }
@@ -297,18 +316,59 @@ export class SandboxInstance {
297
316
  throw new Error("Sandbox name is required");
298
317
  }
299
318
  // Get the existing sandbox to check its status
300
- const sandboxInstance = await this.get(name);
319
+ let sandboxInstance;
320
+ try {
321
+ sandboxInstance = await this.get(name);
322
+ }
323
+ catch (getError) {
324
+ if (isSandboxNotFound(getError)) {
325
+ // The record vanished between the create conflict and this status check
326
+ // (its deletion just finished); give the control plane a beat and retry.
327
+ lastStatus = "vanished";
328
+ if (!finalAttempt) {
329
+ await new Promise((resolve) => setTimeout(resolve, TRANSIENT_STATUS_POLL_MS));
330
+ }
331
+ continue;
332
+ }
333
+ throw getError;
334
+ }
301
335
  // Recreate instead of returning sandbox records that cannot be reused.
302
336
  if (!NON_REUSABLE_SANDBOX_STATUSES.has(sandboxInstance.status ?? "")) {
303
337
  return sandboxInstance;
304
338
  }
339
+ // A delete or deactivation in flight rejects creates until it finishes;
340
+ // wait it out instead of burning the remaining attempts inside the window.
341
+ // No point waiting after the last attempt: nothing will use the result.
342
+ lastStatus = sandboxInstance.status ?? "unknown";
343
+ if (TRANSIENT_SANDBOX_STATUSES.has(lastStatus) && !finalAttempt) {
344
+ await this.waitWhileSandboxDying(name);
345
+ }
305
346
  // Retry creation. We want the same error handling on the retry as creates can race.
306
347
  continue;
307
348
  }
308
349
  throw e;
309
350
  }
310
351
  }
311
- throw new Error(`Unable to create sandbox after ${ATTEMPTS} attempts.`);
352
+ throw new Error(`Unable to create sandbox after ${ATTEMPTS} attempts. Last conflicting status: ${lastStatus}.`);
353
+ }
354
+ // Poll the record until an in-flight delete/deactivation settles (or the record
355
+ // disappears), bounded by TRANSIENT_STATUS_MAX_WAIT_MS. Errors from get (e.g. 404
356
+ // once the record is gone) end the wait: the caller's create retry decides next.
357
+ static async waitWhileSandboxDying(name) {
358
+ const deadline = Date.now() + TRANSIENT_STATUS_MAX_WAIT_MS;
359
+ while (Date.now() < deadline) {
360
+ await new Promise((resolve) => setTimeout(resolve, TRANSIENT_STATUS_POLL_MS));
361
+ try {
362
+ const current = await this.get(name);
363
+ if (!TRANSIENT_SANDBOX_STATUSES.has(current.status ?? "")) {
364
+ return;
365
+ }
366
+ logger.debug(`Sandbox ${name} still ${current.status}; waiting for the record to settle before recreating`);
367
+ }
368
+ catch {
369
+ return;
370
+ }
371
+ }
312
372
  }
313
373
  /* eslint-disable */
314
374
  static async fromSession(session) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blaxel/core",
3
- "version": "0.2.87",
3
+ "version": "0.2.88",
4
4
  "description": "Blaxel Core SDK for TypeScript",
5
5
  "license": "MIT",
6
6
  "author": "Blaxel, INC (https://blaxel.ai)",