veryfront 0.1.280 → 0.1.282

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/esm/deno.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.277",
4
- "version": "0.1.280",
3
+ "version": "0.1.282",
5
4
  "license": "Apache-2.0",
6
5
  "nodeModulesDir": "auto",
7
6
  "workspace": [
@@ -5,6 +5,14 @@ export interface LazySandboxOptions extends SandboxOptions {
5
5
  pollIntervalMs?: number;
6
6
  heartbeatIntervalMs?: number;
7
7
  heartbeatGraceMs?: number;
8
+ controlRequestTimeoutMs?: number;
9
+ execStartTimeoutMs?: number;
10
+ execStartMaxAttempts?: number;
11
+ execStartRetryDelayMs?: number;
12
+ resolveRuntimeEndpoint?: (input: {
13
+ endpoint: string;
14
+ sessionId: string;
15
+ }) => string;
8
16
  }
9
17
  /** Lazily provisions sandbox sessions and keeps them alive while in use. */
10
18
  export declare class LazySandbox {
@@ -15,6 +23,11 @@ export declare class LazySandbox {
15
23
  private readonly pollIntervalMs;
16
24
  private readonly heartbeatIntervalMs;
17
25
  private readonly heartbeatGraceMs;
26
+ private readonly controlRequestTimeoutMs;
27
+ private readonly execStartTimeoutMs;
28
+ private readonly execStartMaxAttempts;
29
+ private readonly execStartRetryDelayMs;
30
+ private readonly resolveRuntimeEndpointOption;
18
31
  private endpoint;
19
32
  private sessionId;
20
33
  private sessionProjectId;
@@ -45,6 +58,7 @@ export declare class LazySandbox {
45
58
  get isActive(): boolean;
46
59
  private bootstrapSession;
47
60
  private resolveReadyEndpoint;
61
+ private waitForReadySession;
48
62
  private touchSession;
49
63
  private startHeartbeatLoop;
50
64
  private stopHeartbeatLoop;
@@ -55,6 +69,13 @@ export declare class LazySandbox {
55
69
  private resolveExecOptions;
56
70
  private resolveCommandJobEndpoint;
57
71
  private updateTrackedCommandJob;
72
+ private startExec;
73
+ private fetchExecStart;
74
+ private fetchControl;
75
+ private waitForExecStartRetry;
76
+ private reprovisionAfterExecStartFailure;
77
+ private resolveRuntimeEndpoint;
78
+ private requireSessionId;
58
79
  private authHeaders;
59
80
  private jsonHeaders;
60
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lazy-sandbox.d.ts","sourceRoot":"","sources":["../../../src/src/sandbox/lazy-sandbox.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lazy-sandbox.d.ts","sourceRoot":"","sources":["../../../src/src/sandbox/lazy-sandbox.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,20 @@
1
1
  import * as dntShim from "../../_dnt.shims.js";
2
2
  import { REQUEST_ERROR } from "../errors/index.js";
3
- import { resolveSandboxApiUrl, resolveSandboxAuthToken, waitForSandboxReady, } from "./sandbox.js";
3
+ import { resolveSandboxApiUrl, resolveSandboxAuthToken, } from "./sandbox.js";
4
4
  const DEFAULT_STARTUP_TIMEOUT_MS = 180_000;
5
5
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
6
6
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
7
7
  const DEFAULT_HEARTBEAT_GRACE_MS = 5_000;
8
+ const DEFAULT_CONTROL_REQUEST_TIMEOUT_MS = 15_000;
9
+ const DEFAULT_EXEC_START_TIMEOUT_MS = 30_000;
10
+ const DEFAULT_EXEC_START_MAX_ATTEMPTS = 3;
11
+ const DEFAULT_EXEC_START_RETRY_DELAY_MS = 1_000;
12
+ const REPROVISIONABLE_EXEC_START_ERROR_CODES = new Set([
13
+ "ECONNREFUSED",
14
+ "ECONNRESET",
15
+ "ENOTFOUND",
16
+ "EHOSTUNREACH",
17
+ ]);
8
18
  /** Lazily provisions sandbox sessions and keeps them alive while in use. */
9
19
  export class LazySandbox {
10
20
  apiUrl;
@@ -14,6 +24,11 @@ export class LazySandbox {
14
24
  pollIntervalMs;
15
25
  heartbeatIntervalMs;
16
26
  heartbeatGraceMs;
27
+ controlRequestTimeoutMs;
28
+ execStartTimeoutMs;
29
+ execStartMaxAttempts;
30
+ execStartRetryDelayMs;
31
+ resolveRuntimeEndpointOption;
17
32
  endpoint = null;
18
33
  sessionId = null;
19
34
  sessionProjectId = null;
@@ -31,6 +46,13 @@ export class LazySandbox {
31
46
  this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
32
47
  this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
33
48
  this.heartbeatGraceMs = options.heartbeatGraceMs ?? DEFAULT_HEARTBEAT_GRACE_MS;
49
+ this.controlRequestTimeoutMs = options.controlRequestTimeoutMs ??
50
+ DEFAULT_CONTROL_REQUEST_TIMEOUT_MS;
51
+ this.execStartTimeoutMs = options.execStartTimeoutMs ?? DEFAULT_EXEC_START_TIMEOUT_MS;
52
+ this.execStartMaxAttempts = options.execStartMaxAttempts ?? DEFAULT_EXEC_START_MAX_ATTEMPTS;
53
+ this.execStartRetryDelayMs = options.execStartRetryDelayMs ??
54
+ DEFAULT_EXEC_START_RETRY_DELAY_MS;
55
+ this.resolveRuntimeEndpointOption = options.resolveRuntimeEndpoint;
34
56
  }
35
57
  async ensure() {
36
58
  if (this.endpoint)
@@ -71,13 +93,17 @@ export class LazySandbox {
71
93
  }
72
94
  async *executeStream(command, options) {
73
95
  await this.touchSession();
74
- const res = await fetch(`${this.requireEndpoint()}/exec`, {
75
- method: "POST",
76
- headers: this.jsonHeaders(),
77
- body: JSON.stringify({ command, ...this.resolveExecOptions(options) }),
78
- });
79
- if (!res.ok) {
80
- throw REQUEST_ERROR.create({ detail: `Exec failed: ${res.status} ${await res.text()}` });
96
+ let res;
97
+ try {
98
+ res = await this.startExec(command, options);
99
+ }
100
+ catch (error) {
101
+ if (!shouldReprovisionAfterExecStartFailure(error)) {
102
+ throw error;
103
+ }
104
+ await this.reprovisionAfterExecStartFailure();
105
+ await this.touchSession();
106
+ res = await this.startExec(command, options);
81
107
  }
82
108
  if (!res.body) {
83
109
  throw new Error("Exec response has no body");
@@ -104,7 +130,7 @@ export class LazySandbox {
104
130
  }
105
131
  async readFile(path) {
106
132
  await this.touchSession();
107
- const res = await fetch(`${this.requireEndpoint()}/file?path=${encodeURIComponent(path)}`, {
133
+ const res = await this.fetchControl(`${this.requireEndpoint()}/file?path=${encodeURIComponent(path)}`, {
108
134
  headers: this.authHeaders(),
109
135
  });
110
136
  if (!res.ok) {
@@ -114,7 +140,7 @@ export class LazySandbox {
114
140
  }
115
141
  async writeFiles(files) {
116
142
  await this.touchSession();
117
- const res = await fetch(`${this.requireEndpoint()}/files`, {
143
+ const res = await this.fetchControl(`${this.requireEndpoint()}/files`, {
118
144
  method: "POST",
119
145
  headers: this.jsonHeaders(),
120
146
  body: JSON.stringify({ files }),
@@ -127,8 +153,8 @@ export class LazySandbox {
127
153
  }
128
154
  async startCommandJob(command, options) {
129
155
  await this.touchSession();
130
- const endpoint = this.requireEndpoint();
131
- const res = await fetch(`${endpoint}/exec/jobs`, {
156
+ const endpoint = this.resolveRuntimeEndpoint();
157
+ const res = await this.fetchControl(`${endpoint}/exec/jobs`, {
132
158
  method: "POST",
133
159
  headers: this.jsonHeaders(),
134
160
  body: JSON.stringify({ command, ...this.resolveExecOptions(options) }),
@@ -144,7 +170,7 @@ export class LazySandbox {
144
170
  }
145
171
  async getCommandJob(jobId) {
146
172
  const endpoint = await this.resolveCommandJobEndpoint(jobId);
147
- const res = await fetch(`${endpoint}/exec/jobs/${jobId}`, {
173
+ const res = await this.fetchControl(`${endpoint}/exec/jobs/${jobId}`, {
148
174
  headers: this.authHeaders(),
149
175
  });
150
176
  if (!res.ok) {
@@ -158,7 +184,7 @@ export class LazySandbox {
158
184
  }
159
185
  async getCommandJobOutput(jobId) {
160
186
  const endpoint = await this.resolveCommandJobEndpoint(jobId);
161
- const res = await fetch(`${endpoint}/exec/jobs/${jobId}/output`, {
187
+ const res = await this.fetchControl(`${endpoint}/exec/jobs/${jobId}/output`, {
162
188
  headers: this.authHeaders(),
163
189
  });
164
190
  if (!res.ok) {
@@ -179,7 +205,7 @@ export class LazySandbox {
179
205
  }
180
206
  async listCommandJobs() {
181
207
  await this.ensure();
182
- const res = await fetch(`${this.requireEndpoint()}/exec/jobs`, {
208
+ const res = await this.fetchControl(`${this.requireEndpoint()}/exec/jobs`, {
183
209
  headers: this.authHeaders(),
184
210
  });
185
211
  if (!res.ok) {
@@ -193,7 +219,7 @@ export class LazySandbox {
193
219
  }
194
220
  async cancelCommandJob(jobId) {
195
221
  const endpoint = await this.resolveCommandJobEndpoint(jobId);
196
- const res = await fetch(`${endpoint}/exec/jobs/${jobId}/cancel`, {
222
+ const res = await this.fetchControl(`${endpoint}/exec/jobs/${jobId}/cancel`, {
197
223
  method: "POST",
198
224
  headers: this.authHeaders(),
199
225
  });
@@ -219,7 +245,7 @@ export class LazySandbox {
219
245
  return;
220
246
  }
221
247
  const promise = (async () => {
222
- const res = await fetch(`${this.apiUrl}/sandbox-sessions/${currentSessionId}/heartbeat`, {
248
+ const res = await this.fetchControl(`${this.apiUrl}/sandbox-sessions/${currentSessionId}/heartbeat`, {
223
249
  method: "POST",
224
250
  headers: this.authHeaders(),
225
251
  });
@@ -287,7 +313,7 @@ export class LazySandbox {
287
313
  }
288
314
  async bootstrapSession() {
289
315
  const projectId = this.resolveProjectId();
290
- const res = await fetch(`${this.apiUrl}/sandbox-sessions`, {
316
+ const res = await this.fetchControl(`${this.apiUrl}/sandbox-sessions`, {
291
317
  method: "POST",
292
318
  headers: this.jsonHeaders(),
293
319
  body: JSON.stringify(projectId ? { project_id: projectId } : {}),
@@ -319,23 +345,29 @@ export class LazySandbox {
319
345
  if (session.status === "running") {
320
346
  return session.endpoint;
321
347
  }
322
- await waitForSandboxReady({
323
- apiUrl: this.apiUrl,
324
- id: session.id,
325
- authToken: this.authToken,
326
- maxWaitMs: this.startupTimeoutMs,
327
- pollIntervalMs: this.pollIntervalMs,
328
- });
329
- const res = await fetch(`${this.apiUrl}/sandbox-sessions/${session.id}`, {
330
- headers: this.authHeaders(),
331
- });
332
- if (!res.ok) {
333
- throw REQUEST_ERROR.create({
334
- detail: `Failed to get sandbox: ${res.status} ${await res.text()}`,
348
+ return (await this.waitForReadySession(session.id)).endpoint;
349
+ }
350
+ async waitForReadySession(sessionId) {
351
+ const start = Date.now();
352
+ while (Date.now() - start < this.startupTimeoutMs) {
353
+ await new Promise((resolve) => dntShim.setTimeout(resolve, this.pollIntervalMs));
354
+ const res = await this.fetchControl(`${this.apiUrl}/sandbox-sessions/${sessionId}`, {
355
+ headers: this.authHeaders(),
335
356
  });
357
+ if (!res.ok) {
358
+ continue;
359
+ }
360
+ const session = await res.json();
361
+ if (session.status === "running") {
362
+ return session;
363
+ }
364
+ if (session.status === "error" || session.status === "deleting") {
365
+ throw REQUEST_ERROR.create({
366
+ detail: `Sandbox failed to start: status=${session.status}`,
367
+ });
368
+ }
336
369
  }
337
- const nextSession = await res.json();
338
- return nextSession.endpoint;
370
+ throw REQUEST_ERROR.create({ detail: "Sandbox did not become ready within timeout" });
339
371
  }
340
372
  async touchSession() {
341
373
  const projectId = this.resolveProjectId();
@@ -365,7 +397,7 @@ export class LazySandbox {
365
397
  this.heartbeatTimer = null;
366
398
  }
367
399
  async deleteSession(sessionId) {
368
- await fetch(`${this.apiUrl}/sandbox-sessions/${sessionId}`, {
400
+ await this.fetchControl(`${this.apiUrl}/sandbox-sessions/${sessionId}`, {
369
401
  method: "DELETE",
370
402
  headers: this.authHeaders(),
371
403
  });
@@ -400,7 +432,7 @@ export class LazySandbox {
400
432
  return trackedEndpoint;
401
433
  }
402
434
  await this.ensure();
403
- return this.requireEndpoint();
435
+ return this.resolveRuntimeEndpoint();
404
436
  }
405
437
  updateTrackedCommandJob(job, endpoint) {
406
438
  if (job.status === "running") {
@@ -415,6 +447,61 @@ export class LazySandbox {
415
447
  this.startHeartbeatLoop();
416
448
  }
417
449
  }
450
+ async startExec(command, options) {
451
+ const endpoint = this.resolveRuntimeEndpoint();
452
+ const body = JSON.stringify({ command, ...this.resolveExecOptions(options) });
453
+ for (let attempt = 1; attempt <= this.execStartMaxAttempts; attempt += 1) {
454
+ try {
455
+ const res = await this.fetchExecStart(`${endpoint}/exec`, {
456
+ method: "POST",
457
+ headers: this.jsonHeaders(),
458
+ body,
459
+ });
460
+ if (res.ok) {
461
+ return res;
462
+ }
463
+ if (isRetryableExecStartStatus(res.status) && attempt < this.execStartMaxAttempts) {
464
+ await this.waitForExecStartRetry();
465
+ continue;
466
+ }
467
+ throw REQUEST_ERROR.create({ detail: `Exec failed: ${res.status} ${await res.text()}` });
468
+ }
469
+ catch (error) {
470
+ if (!isRetryableExecStartError(error) || attempt >= this.execStartMaxAttempts) {
471
+ throw error;
472
+ }
473
+ await this.waitForExecStartRetry();
474
+ }
475
+ }
476
+ throw new Error("Sandbox exec failed before a request was made");
477
+ }
478
+ async fetchExecStart(url, init) {
479
+ return fetchWithTimeout(url, this.execStartTimeoutMs, init);
480
+ }
481
+ async fetchControl(url, init = {}) {
482
+ return fetchWithTimeout(url, this.controlRequestTimeoutMs, init);
483
+ }
484
+ waitForExecStartRetry() {
485
+ return new Promise((resolve) => dntShim.setTimeout(resolve, this.execStartRetryDelayMs));
486
+ }
487
+ async reprovisionAfterExecStartFailure() {
488
+ const sessionId = this.sessionId;
489
+ if (!sessionId)
490
+ return;
491
+ await this.deleteSession(sessionId);
492
+ this.resetSessionState(sessionId);
493
+ }
494
+ resolveRuntimeEndpoint() {
495
+ const endpoint = this.requireEndpoint();
496
+ const sessionId = this.requireSessionId();
497
+ return this.resolveRuntimeEndpointOption?.({ endpoint, sessionId }) ?? endpoint;
498
+ }
499
+ requireSessionId() {
500
+ if (!this.sessionId) {
501
+ throw new Error("Sandbox session unavailable");
502
+ }
503
+ return this.sessionId;
504
+ }
418
505
  authHeaders() {
419
506
  return { Authorization: `Bearer ${this.authToken}` };
420
507
  }
@@ -425,6 +512,36 @@ export class LazySandbox {
425
512
  };
426
513
  }
427
514
  }
515
+ function isRetryableExecStartStatus(status) {
516
+ return status === 502 || status === 503 || status === 504;
517
+ }
518
+ function isRetryableExecStartError(error) {
519
+ return error instanceof Error && /fetch failed/i.test(error.message);
520
+ }
521
+ function shouldReprovisionAfterExecStartFailure(error) {
522
+ if (!(error instanceof Error)) {
523
+ return false;
524
+ }
525
+ const cause = error.cause;
526
+ if (typeof cause !== "object" || cause === null || !("code" in cause)) {
527
+ return false;
528
+ }
529
+ return typeof cause.code === "string" &&
530
+ REPROVISIONABLE_EXEC_START_ERROR_CODES.has(cause.code);
531
+ }
532
+ async function fetchWithTimeout(url, timeoutMs, init = {}) {
533
+ if (timeoutMs <= 0) {
534
+ return await fetch(url, init);
535
+ }
536
+ const controller = new AbortController();
537
+ const timeout = dntShim.setTimeout(() => controller.abort(), timeoutMs);
538
+ try {
539
+ return await fetch(url, { ...init, signal: controller.signal });
540
+ }
541
+ finally {
542
+ clearTimeout(timeout);
543
+ }
544
+ }
428
545
  function mapCommandJob(json) {
429
546
  return {
430
547
  id: json.id,
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.280";
1
+ export declare const VERSION = "0.1.282";
2
2
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.280";
3
+ export const VERSION = "0.1.282";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.280",
3
+ "version": "0.1.282",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.277",
4
- "version": "0.1.280",
3
+ "version": "0.1.282",
5
4
  "license": "Apache-2.0",
6
5
  "nodeModulesDir": "auto",
7
6
  "workspace": [
@@ -10,7 +10,6 @@ import {
10
10
  resolveSandboxApiUrl,
11
11
  resolveSandboxAuthToken,
12
12
  type SandboxOptions,
13
- waitForSandboxReady,
14
13
  } from "./sandbox.js";
15
14
 
16
15
  export interface LazySandboxOptions extends SandboxOptions {
@@ -19,6 +18,11 @@ export interface LazySandboxOptions extends SandboxOptions {
19
18
  pollIntervalMs?: number;
20
19
  heartbeatIntervalMs?: number;
21
20
  heartbeatGraceMs?: number;
21
+ controlRequestTimeoutMs?: number;
22
+ execStartTimeoutMs?: number;
23
+ execStartMaxAttempts?: number;
24
+ execStartRetryDelayMs?: number;
25
+ resolveRuntimeEndpoint?: (input: { endpoint: string; sessionId: string }) => string;
22
26
  }
23
27
 
24
28
  interface SandboxSessionRecord {
@@ -31,6 +35,16 @@ const DEFAULT_STARTUP_TIMEOUT_MS = 180_000;
31
35
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
32
36
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
33
37
  const DEFAULT_HEARTBEAT_GRACE_MS = 5_000;
38
+ const DEFAULT_CONTROL_REQUEST_TIMEOUT_MS = 15_000;
39
+ const DEFAULT_EXEC_START_TIMEOUT_MS = 30_000;
40
+ const DEFAULT_EXEC_START_MAX_ATTEMPTS = 3;
41
+ const DEFAULT_EXEC_START_RETRY_DELAY_MS = 1_000;
42
+ const REPROVISIONABLE_EXEC_START_ERROR_CODES = new Set([
43
+ "ECONNREFUSED",
44
+ "ECONNRESET",
45
+ "ENOTFOUND",
46
+ "EHOSTUNREACH",
47
+ ]);
34
48
 
35
49
  /** Lazily provisions sandbox sessions and keeps them alive while in use. */
36
50
  export class LazySandbox {
@@ -41,6 +55,16 @@ export class LazySandbox {
41
55
  private readonly pollIntervalMs: number;
42
56
  private readonly heartbeatIntervalMs: number;
43
57
  private readonly heartbeatGraceMs: number;
58
+ private readonly controlRequestTimeoutMs: number;
59
+ private readonly execStartTimeoutMs: number;
60
+ private readonly execStartMaxAttempts: number;
61
+ private readonly execStartRetryDelayMs: number;
62
+ private readonly resolveRuntimeEndpointOption:
63
+ | ((input: {
64
+ endpoint: string;
65
+ sessionId: string;
66
+ }) => string)
67
+ | undefined;
44
68
 
45
69
  private endpoint: string | null = null;
46
70
  private sessionId: string | null = null;
@@ -60,6 +84,13 @@ export class LazySandbox {
60
84
  this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
61
85
  this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
62
86
  this.heartbeatGraceMs = options.heartbeatGraceMs ?? DEFAULT_HEARTBEAT_GRACE_MS;
87
+ this.controlRequestTimeoutMs = options.controlRequestTimeoutMs ??
88
+ DEFAULT_CONTROL_REQUEST_TIMEOUT_MS;
89
+ this.execStartTimeoutMs = options.execStartTimeoutMs ?? DEFAULT_EXEC_START_TIMEOUT_MS;
90
+ this.execStartMaxAttempts = options.execStartMaxAttempts ?? DEFAULT_EXEC_START_MAX_ATTEMPTS;
91
+ this.execStartRetryDelayMs = options.execStartRetryDelayMs ??
92
+ DEFAULT_EXEC_START_RETRY_DELAY_MS;
93
+ this.resolveRuntimeEndpointOption = options.resolveRuntimeEndpoint;
63
94
  }
64
95
 
65
96
  async ensure(): Promise<void> {
@@ -105,15 +136,17 @@ export class LazySandbox {
105
136
 
106
137
  async *executeStream(command: string, options?: ExecOptions): AsyncGenerator<ExecStreamEvent> {
107
138
  await this.touchSession();
139
+ let res: Response;
140
+ try {
141
+ res = await this.startExec(command, options);
142
+ } catch (error) {
143
+ if (!shouldReprovisionAfterExecStartFailure(error)) {
144
+ throw error;
145
+ }
108
146
 
109
- const res = await fetch(`${this.requireEndpoint()}/exec`, {
110
- method: "POST",
111
- headers: this.jsonHeaders(),
112
- body: JSON.stringify({ command, ...this.resolveExecOptions(options) }),
113
- });
114
-
115
- if (!res.ok) {
116
- throw REQUEST_ERROR.create({ detail: `Exec failed: ${res.status} ${await res.text()}` });
147
+ await this.reprovisionAfterExecStartFailure();
148
+ await this.touchSession();
149
+ res = await this.startExec(command, options);
117
150
  }
118
151
 
119
152
  if (!res.body) {
@@ -146,9 +179,12 @@ export class LazySandbox {
146
179
  async readFile(path: string): Promise<string> {
147
180
  await this.touchSession();
148
181
 
149
- const res = await fetch(`${this.requireEndpoint()}/file?path=${encodeURIComponent(path)}`, {
150
- headers: this.authHeaders(),
151
- });
182
+ const res = await this.fetchControl(
183
+ `${this.requireEndpoint()}/file?path=${encodeURIComponent(path)}`,
184
+ {
185
+ headers: this.authHeaders(),
186
+ },
187
+ );
152
188
 
153
189
  if (!res.ok) {
154
190
  throw REQUEST_ERROR.create({ detail: `Read file failed: ${res.status} ${await res.text()}` });
@@ -160,7 +196,7 @@ export class LazySandbox {
160
196
  async writeFiles(files: Array<{ path: string; content: string }>): Promise<void> {
161
197
  await this.touchSession();
162
198
 
163
- const res = await fetch(`${this.requireEndpoint()}/files`, {
199
+ const res = await this.fetchControl(`${this.requireEndpoint()}/files`, {
164
200
  method: "POST",
165
201
  headers: this.jsonHeaders(),
166
202
  body: JSON.stringify({ files }),
@@ -175,9 +211,9 @@ export class LazySandbox {
175
211
 
176
212
  async startCommandJob(command: string, options?: ExecOptions): Promise<CommandJob> {
177
213
  await this.touchSession();
178
- const endpoint = this.requireEndpoint();
214
+ const endpoint = this.resolveRuntimeEndpoint();
179
215
 
180
- const res = await fetch(`${endpoint}/exec/jobs`, {
216
+ const res = await this.fetchControl(`${endpoint}/exec/jobs`, {
181
217
  method: "POST",
182
218
  headers: this.jsonHeaders(),
183
219
  body: JSON.stringify({ command, ...this.resolveExecOptions(options) }),
@@ -197,7 +233,7 @@ export class LazySandbox {
197
233
  async getCommandJob(jobId: string): Promise<CommandJob> {
198
234
  const endpoint = await this.resolveCommandJobEndpoint(jobId);
199
235
 
200
- const res = await fetch(`${endpoint}/exec/jobs/${jobId}`, {
236
+ const res = await this.fetchControl(`${endpoint}/exec/jobs/${jobId}`, {
201
237
  headers: this.authHeaders(),
202
238
  });
203
239
 
@@ -215,7 +251,7 @@ export class LazySandbox {
215
251
  async getCommandJobOutput(jobId: string): Promise<CommandJobOutput> {
216
252
  const endpoint = await this.resolveCommandJobEndpoint(jobId);
217
253
 
218
- const res = await fetch(`${endpoint}/exec/jobs/${jobId}/output`, {
254
+ const res = await this.fetchControl(`${endpoint}/exec/jobs/${jobId}/output`, {
219
255
  headers: this.authHeaders(),
220
256
  });
221
257
 
@@ -240,7 +276,7 @@ export class LazySandbox {
240
276
  async listCommandJobs(): Promise<CommandJob[]> {
241
277
  await this.ensure();
242
278
 
243
- const res = await fetch(`${this.requireEndpoint()}/exec/jobs`, {
279
+ const res = await this.fetchControl(`${this.requireEndpoint()}/exec/jobs`, {
244
280
  headers: this.authHeaders(),
245
281
  });
246
282
 
@@ -258,7 +294,7 @@ export class LazySandbox {
258
294
  async cancelCommandJob(jobId: string): Promise<CommandJob> {
259
295
  const endpoint = await this.resolveCommandJobEndpoint(jobId);
260
296
 
261
- const res = await fetch(`${endpoint}/exec/jobs/${jobId}/cancel`, {
297
+ const res = await this.fetchControl(`${endpoint}/exec/jobs/${jobId}/cancel`, {
262
298
  method: "POST",
263
299
  headers: this.authHeaders(),
264
300
  });
@@ -291,10 +327,13 @@ export class LazySandbox {
291
327
  }
292
328
 
293
329
  const promise = (async () => {
294
- const res = await fetch(`${this.apiUrl}/sandbox-sessions/${currentSessionId}/heartbeat`, {
295
- method: "POST",
296
- headers: this.authHeaders(),
297
- });
330
+ const res = await this.fetchControl(
331
+ `${this.apiUrl}/sandbox-sessions/${currentSessionId}/heartbeat`,
332
+ {
333
+ method: "POST",
334
+ headers: this.authHeaders(),
335
+ },
336
+ );
298
337
 
299
338
  if (!res.ok) {
300
339
  if (this.sessionId === currentSessionId) {
@@ -371,7 +410,7 @@ export class LazySandbox {
371
410
 
372
411
  private async bootstrapSession(): Promise<void> {
373
412
  const projectId = this.resolveProjectId();
374
- const res = await fetch(`${this.apiUrl}/sandbox-sessions`, {
413
+ const res = await this.fetchControl(`${this.apiUrl}/sandbox-sessions`, {
375
414
  method: "POST",
376
415
  headers: this.jsonHeaders(),
377
416
  body: JSON.stringify(projectId ? { project_id: projectId } : {}),
@@ -407,26 +446,35 @@ export class LazySandbox {
407
446
  return session.endpoint;
408
447
  }
409
448
 
410
- await waitForSandboxReady({
411
- apiUrl: this.apiUrl,
412
- id: session.id,
413
- authToken: this.authToken,
414
- maxWaitMs: this.startupTimeoutMs,
415
- pollIntervalMs: this.pollIntervalMs,
416
- });
449
+ return (await this.waitForReadySession(session.id)).endpoint;
450
+ }
417
451
 
418
- const res = await fetch(`${this.apiUrl}/sandbox-sessions/${session.id}`, {
419
- headers: this.authHeaders(),
420
- });
452
+ private async waitForReadySession(sessionId: string): Promise<SandboxSessionRecord> {
453
+ const start = Date.now();
421
454
 
422
- if (!res.ok) {
423
- throw REQUEST_ERROR.create({
424
- detail: `Failed to get sandbox: ${res.status} ${await res.text()}`,
455
+ while (Date.now() - start < this.startupTimeoutMs) {
456
+ await new Promise((resolve) => dntShim.setTimeout(resolve, this.pollIntervalMs));
457
+
458
+ const res = await this.fetchControl(`${this.apiUrl}/sandbox-sessions/${sessionId}`, {
459
+ headers: this.authHeaders(),
425
460
  });
461
+
462
+ if (!res.ok) {
463
+ continue;
464
+ }
465
+
466
+ const session = await res.json() as SandboxSessionRecord;
467
+ if (session.status === "running") {
468
+ return session;
469
+ }
470
+ if (session.status === "error" || session.status === "deleting") {
471
+ throw REQUEST_ERROR.create({
472
+ detail: `Sandbox failed to start: status=${session.status}`,
473
+ });
474
+ }
426
475
  }
427
476
 
428
- const nextSession = await res.json();
429
- return nextSession.endpoint;
477
+ throw REQUEST_ERROR.create({ detail: "Sandbox did not become ready within timeout" });
430
478
  }
431
479
 
432
480
  private async touchSession(): Promise<void> {
@@ -460,7 +508,7 @@ export class LazySandbox {
460
508
  }
461
509
 
462
510
  private async deleteSession(sessionId: string): Promise<void> {
463
- await fetch(`${this.apiUrl}/sandbox-sessions/${sessionId}`, {
511
+ await this.fetchControl(`${this.apiUrl}/sandbox-sessions/${sessionId}`, {
464
512
  method: "DELETE",
465
513
  headers: this.authHeaders(),
466
514
  });
@@ -501,7 +549,7 @@ export class LazySandbox {
501
549
  }
502
550
 
503
551
  await this.ensure();
504
- return this.requireEndpoint();
552
+ return this.resolveRuntimeEndpoint();
505
553
  }
506
554
 
507
555
  private updateTrackedCommandJob(job: Pick<CommandJob, "id" | "status">, endpoint: string): void {
@@ -520,6 +568,74 @@ export class LazySandbox {
520
568
  }
521
569
  }
522
570
 
571
+ private async startExec(command: string, options?: ExecOptions): Promise<Response> {
572
+ const endpoint = this.resolveRuntimeEndpoint();
573
+ const body = JSON.stringify({ command, ...this.resolveExecOptions(options) });
574
+
575
+ for (let attempt = 1; attempt <= this.execStartMaxAttempts; attempt += 1) {
576
+ try {
577
+ const res = await this.fetchExecStart(`${endpoint}/exec`, {
578
+ method: "POST",
579
+ headers: this.jsonHeaders(),
580
+ body,
581
+ });
582
+
583
+ if (res.ok) {
584
+ return res;
585
+ }
586
+
587
+ if (isRetryableExecStartStatus(res.status) && attempt < this.execStartMaxAttempts) {
588
+ await this.waitForExecStartRetry();
589
+ continue;
590
+ }
591
+
592
+ throw REQUEST_ERROR.create({ detail: `Exec failed: ${res.status} ${await res.text()}` });
593
+ } catch (error) {
594
+ if (!isRetryableExecStartError(error) || attempt >= this.execStartMaxAttempts) {
595
+ throw error;
596
+ }
597
+
598
+ await this.waitForExecStartRetry();
599
+ }
600
+ }
601
+
602
+ throw new Error("Sandbox exec failed before a request was made");
603
+ }
604
+
605
+ private async fetchExecStart(url: string, init: RequestInit): Promise<Response> {
606
+ return fetchWithTimeout(url, this.execStartTimeoutMs, init);
607
+ }
608
+
609
+ private async fetchControl(url: string, init: RequestInit = {}): Promise<Response> {
610
+ return fetchWithTimeout(url, this.controlRequestTimeoutMs, init);
611
+ }
612
+
613
+ private waitForExecStartRetry(): Promise<void> {
614
+ return new Promise((resolve) => dntShim.setTimeout(resolve, this.execStartRetryDelayMs));
615
+ }
616
+
617
+ private async reprovisionAfterExecStartFailure(): Promise<void> {
618
+ const sessionId = this.sessionId;
619
+ if (!sessionId) return;
620
+
621
+ await this.deleteSession(sessionId);
622
+ this.resetSessionState(sessionId);
623
+ }
624
+
625
+ private resolveRuntimeEndpoint(): string {
626
+ const endpoint = this.requireEndpoint();
627
+ const sessionId = this.requireSessionId();
628
+ return this.resolveRuntimeEndpointOption?.({ endpoint, sessionId }) ?? endpoint;
629
+ }
630
+
631
+ private requireSessionId(): string {
632
+ if (!this.sessionId) {
633
+ throw new Error("Sandbox session unavailable");
634
+ }
635
+
636
+ return this.sessionId;
637
+ }
638
+
523
639
  private authHeaders(): HeadersInit {
524
640
  return { Authorization: `Bearer ${this.authToken}` };
525
641
  }
@@ -532,6 +648,47 @@ export class LazySandbox {
532
648
  }
533
649
  }
534
650
 
651
+ function isRetryableExecStartStatus(status: number): boolean {
652
+ return status === 502 || status === 503 || status === 504;
653
+ }
654
+
655
+ function isRetryableExecStartError(error: unknown): boolean {
656
+ return error instanceof Error && /fetch failed/i.test(error.message);
657
+ }
658
+
659
+ function shouldReprovisionAfterExecStartFailure(error: unknown): boolean {
660
+ if (!(error instanceof Error)) {
661
+ return false;
662
+ }
663
+
664
+ const cause = error.cause;
665
+ if (typeof cause !== "object" || cause === null || !("code" in cause)) {
666
+ return false;
667
+ }
668
+
669
+ return typeof cause.code === "string" &&
670
+ REPROVISIONABLE_EXEC_START_ERROR_CODES.has(cause.code);
671
+ }
672
+
673
+ async function fetchWithTimeout(
674
+ url: string,
675
+ timeoutMs: number,
676
+ init: RequestInit = {},
677
+ ): Promise<Response> {
678
+ if (timeoutMs <= 0) {
679
+ return await fetch(url, init);
680
+ }
681
+
682
+ const controller = new AbortController();
683
+ const timeout = dntShim.setTimeout(() => controller.abort(), timeoutMs);
684
+
685
+ try {
686
+ return await fetch(url, { ...init, signal: controller.signal });
687
+ } finally {
688
+ clearTimeout(timeout);
689
+ }
690
+ }
691
+
535
692
  function mapCommandJob(json: Record<string, unknown>): CommandJob {
536
693
  return {
537
694
  id: json.id as string,
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.280";
3
+ export const VERSION = "0.1.282";