@blaxel/core 0.2.87-preview.171 → 0.2.87-preview.175

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.
Files changed (37) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/common/h2fetch.js +54 -17
  3. package/dist/cjs/common/settings.js +29 -3
  4. package/dist/cjs/common/transient-retry.js +145 -0
  5. package/dist/cjs/sandbox/drive/drive.js +11 -6
  6. package/dist/cjs/sandbox/filesystem/filesystem.js +126 -158
  7. package/dist/cjs/sandbox/process/process.js +29 -16
  8. package/dist/cjs/types/common/h2fetch.d.ts +7 -0
  9. package/dist/cjs/types/common/settings.d.ts +22 -2
  10. package/dist/cjs/types/common/transient-retry.d.ts +30 -0
  11. package/dist/cjs/types/sandbox/filesystem/filesystem.d.ts +3 -0
  12. package/dist/cjs-browser/.tsbuildinfo +1 -1
  13. package/dist/cjs-browser/common/h2fetch.js +1 -0
  14. package/dist/cjs-browser/common/settings.js +29 -3
  15. package/dist/cjs-browser/common/transient-retry.js +145 -0
  16. package/dist/cjs-browser/sandbox/drive/drive.js +11 -6
  17. package/dist/cjs-browser/sandbox/filesystem/filesystem.js +126 -158
  18. package/dist/cjs-browser/sandbox/process/process.js +29 -16
  19. package/dist/cjs-browser/types/common/h2fetch.d.ts +7 -0
  20. package/dist/cjs-browser/types/common/settings.d.ts +22 -2
  21. package/dist/cjs-browser/types/common/transient-retry.d.ts +30 -0
  22. package/dist/cjs-browser/types/sandbox/filesystem/filesystem.d.ts +3 -0
  23. package/dist/esm/.tsbuildinfo +1 -1
  24. package/dist/esm/common/h2fetch.js +53 -17
  25. package/dist/esm/common/settings.js +29 -3
  26. package/dist/esm/common/transient-retry.js +141 -0
  27. package/dist/esm/sandbox/drive/drive.js +11 -6
  28. package/dist/esm/sandbox/filesystem/filesystem.js +124 -157
  29. package/dist/esm/sandbox/process/process.js +29 -16
  30. package/dist/esm-browser/.tsbuildinfo +1 -1
  31. package/dist/esm-browser/common/h2fetch.js +1 -0
  32. package/dist/esm-browser/common/settings.js +29 -3
  33. package/dist/esm-browser/common/transient-retry.js +141 -0
  34. package/dist/esm-browser/sandbox/drive/drive.js +11 -6
  35. package/dist/esm-browser/sandbox/filesystem/filesystem.js +124 -157
  36. package/dist/esm-browser/sandbox/process/process.js +29 -16
  37. package/package.json +1 -1
@@ -3,3 +3,4 @@ export function createH2Fetch(session) { return (input) => globalThis.fetch(inpu
3
3
  export function createPoolBackedH2Fetch(pool, domain) { return (input) => globalThis.fetch(input); }
4
4
  export function h2RequestDirect(session, url, init) { return globalThis.fetch(url, init); }
5
5
  export function h2RequestDirectFromPool(pool, domain, url, init) { return globalThis.fetch(url, init); }
6
+ export async function withUploadSlot(domain, fn) { return fn(); }
@@ -28,8 +28,8 @@ function missingCredentialsMessage() {
28
28
  return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
29
29
  }
30
30
  // Build info - these placeholders are replaced at build time by build:replace-imports
31
- const BUILD_VERSION = "0.2.87-preview.171";
32
- const BUILD_COMMIT = "06530fbea3b8ff89d51d0b834de6725ad5ad80ee";
31
+ const BUILD_VERSION = "0.2.87-preview.175";
32
+ const BUILD_COMMIT = "31085636cd9c2557198ee7399e7794e945c21687";
33
33
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
34
34
  const BLAXEL_API_VERSION = "2026-04-16";
35
35
  // Cache for config.yaml tracking value
@@ -263,6 +263,19 @@ class Settings {
263
263
  }
264
264
  return 0;
265
265
  }
266
+ get maxConcurrentUploadH2Requests() {
267
+ if (typeof this.config.maxConcurrentUploadH2Requests === "number") {
268
+ return this.config.maxConcurrentUploadH2Requests;
269
+ }
270
+ const value = env_js_1.env.BL_MAX_UPLOAD_H2_INFLIGHT;
271
+ if (value) {
272
+ const parsed = parseInt(value, 10);
273
+ if (!Number.isNaN(parsed)) {
274
+ return parsed;
275
+ }
276
+ }
277
+ return 2;
278
+ }
266
279
  get fsPartRetries() {
267
280
  if (typeof this.config.fsPartRetries === "number") {
268
281
  return this.config.fsPartRetries;
@@ -274,7 +287,20 @@ class Settings {
274
287
  return parsed;
275
288
  }
276
289
  }
277
- return 0;
290
+ return 3;
291
+ }
292
+ get sandboxReadRetries() {
293
+ if (typeof this.config.sandboxReadRetries === "number") {
294
+ return this.config.sandboxReadRetries;
295
+ }
296
+ const value = env_js_1.env.BL_SANDBOX_READ_RETRIES;
297
+ if (value) {
298
+ const parsed = parseInt(value, 10);
299
+ if (!Number.isNaN(parsed)) {
300
+ return parsed;
301
+ }
302
+ }
303
+ return 5;
278
304
  }
279
305
  /**
280
306
  * Fail fast with a clear, actionable error when credentials are missing or
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isTransientResetError = isTransientResetError;
4
+ exports.retryOnTransientReset = retryOnTransientReset;
5
+ const settings_js_1 = require("./settings.js");
6
+ // Markers that, when present anywhere in the error chain, are unambiguous
7
+ // signals of a transient HTTP/2 stream reset or connection drop. These are
8
+ // protocol/transport level codes, not application payloads, so substring
9
+ // matching them does not over-match a server-sent error body. Each entry is
10
+ // matched case-sensitively against the error message and its cause.
11
+ //
12
+ // Deliberately excluded: bare "INTERNAL_ERROR" and "fetch failed". Both are
13
+ // too generic on their own (an application 500 body or any failed fetch would
14
+ // match), so we only treat them as transient when paired with a transport
15
+ // error code on the cause (see isTransientResetError).
16
+ const TRANSIENT_RESET_MARKERS = [
17
+ "ENHANCE_YOUR_CALM", // H2 flow-control backpressure reset
18
+ "NGHTTP2_INTERNAL_ERROR", // H2 internal stream error (qualified form)
19
+ "ERR_HTTP2", // node http2 error code family
20
+ "GOAWAY", // peer is draining the connection
21
+ "HTTP/2 session closed before response", // thrown by our own h2 transport
22
+ "HTTP/2 session sent GOAWAY before response",
23
+ ];
24
+ // Node-level error codes (from `error.code` / `error.cause.code`) that mean
25
+ // the connection itself dropped mid-flight and the request never completed.
26
+ // These are safe to retry for an idempotent request.
27
+ const TRANSIENT_ERROR_CODES = new Set([
28
+ "ECONNRESET",
29
+ "ECONNREFUSED",
30
+ "ETIMEDOUT",
31
+ "EPIPE",
32
+ "ERR_HTTP2_STREAM_ERROR",
33
+ "ERR_HTTP2_GOAWAY_SESSION",
34
+ "ERR_HTTP2_SESSION_ERROR",
35
+ ]);
36
+ function collectErrorText(error) {
37
+ const messages = [];
38
+ const codes = [];
39
+ // Walk the error -> cause chain (bounded) so a transport error wrapped by a
40
+ // higher-level "fetch failed" is still classified correctly.
41
+ let current = error;
42
+ for (let depth = 0; depth < 5 && current && typeof current === "object"; depth++) {
43
+ const node = current;
44
+ if (typeof node.message === "string")
45
+ messages.push(node.message);
46
+ if (typeof node.code === "string")
47
+ codes.push(node.code);
48
+ current = node.cause;
49
+ }
50
+ return { messages, codes };
51
+ }
52
+ // True when the error chain carries a real HTTP response (a status came back
53
+ // from the server). Such an error is an APPLICATION response — never a transport
54
+ // reset — even if the server's error body text happens to contain a reset marker
55
+ // like "GOAWAY" or "ERR_HTTP2". Guarding on this stops a marker-bearing 4xx/5xx
56
+ // body from being misread as transient and retried (the over-match Codex flagged
57
+ // for the now default-on idempotent-read retry).
58
+ function hasHttpResponseStatus(error) {
59
+ let current = error;
60
+ for (let depth = 0; depth < 5 && current && typeof current === "object"; depth++) {
61
+ const node = current;
62
+ if (typeof node.status === "number")
63
+ return true;
64
+ if (node.response && typeof node.response === "object" &&
65
+ typeof node.response.status === "number") {
66
+ return true;
67
+ }
68
+ current = node.cause;
69
+ }
70
+ return false;
71
+ }
72
+ /**
73
+ * True only for transport-level resets/drops that are safe to retry on an
74
+ * IDEMPOTENT request (transient HTTP/2 stream reset, GOAWAY, or a dropped
75
+ * connection). Application errors (4xx/5xx — even when their body text contains
76
+ * a marker word) and a bare "fetch failed" with no transport code are
77
+ * deliberately NOT transient, so auto-retry never masks a real server error or
78
+ * duplicates a non-idempotent call.
79
+ *
80
+ * This is the single classifier shared by the upload-part retry (ENG-2680) and
81
+ * the idempotent sandbox-op retry (read/list/etc.), so both judge "transient"
82
+ * identically.
83
+ */
84
+ function isTransientResetError(error) {
85
+ if (!error || typeof error !== "object") {
86
+ return false;
87
+ }
88
+ // An error that carries an HTTP response is an application error, not a
89
+ // transport reset — never retry it on the strength of a marker in its body.
90
+ if (hasHttpResponseStatus(error)) {
91
+ return false;
92
+ }
93
+ const { messages, codes } = collectErrorText(error);
94
+ // 1. An explicit transient transport error code anywhere in the chain.
95
+ if (codes.some((code) => TRANSIENT_ERROR_CODES.has(code))) {
96
+ return true;
97
+ }
98
+ // 2. An unambiguous protocol-level reset marker in any message.
99
+ if (messages.some((text) => TRANSIENT_RESET_MARKERS.some((marker) => text.includes(marker)))) {
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+ const DEFAULT_BASE_DELAY_MS = 200;
105
+ const DEFAULT_MAX_DELAY_MS = 2000;
106
+ // Exponential backoff with full-jitter on top of one base delay, capped so a
107
+ // single wait never blocks unreasonably long. Exponential (rather than linear)
108
+ // gives a later attempt room to span a multi-second sandbox cold-start/standby
109
+ // wake, which is the window a first-call reset falls into, while early attempts
110
+ // stay fast for the common quick-reset case.
111
+ function backoffDelayMs(attempt, baseDelayMs, maxDelayMs) {
112
+ const exponential = baseDelayMs * Math.pow(2, attempt - 1);
113
+ const capped = Math.min(exponential, maxDelayMs);
114
+ const jitter = Math.floor(Math.random() * baseDelayMs);
115
+ return capped + jitter;
116
+ }
117
+ /**
118
+ * Run `fn`, retrying only on transient transport resets (see
119
+ * isTransientResetError) with exponential backoff. Caller owns idempotency:
120
+ * ONLY wrap idempotent operations (GET-shaped reads/lists, or an idempotent
121
+ * PUT of the same bytes) — never a non-idempotent POST such as process.exec,
122
+ * which would duplicate the side effect (ENG-2340).
123
+ *
124
+ * Defaults to `settings.sandboxReadRetries` (the higher idempotent-read budget,
125
+ * sized for a multi-second standby wake). The upload path passes
126
+ * `{ retries: settings.fsPartRetries }` to keep its own (lower) budget.
127
+ */
128
+ async function retryOnTransientReset(fn, options = {}) {
129
+ const retries = options.retries ?? settings_js_1.settings.sandboxReadRetries;
130
+ const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
131
+ const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
132
+ let attempt = 0;
133
+ for (;;) {
134
+ try {
135
+ return await fn();
136
+ }
137
+ catch (error) {
138
+ attempt++;
139
+ if (retries <= 0 || attempt > retries || !isTransientResetError(error)) {
140
+ throw error;
141
+ }
142
+ await new Promise((resolve) => setTimeout(resolve, backoffDelayMs(attempt, baseDelayMs, maxDelayMs)));
143
+ }
144
+ }
145
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SandboxDrive = void 0;
4
4
  const action_js_1 = require("../action.js");
5
+ const transient_retry_js_1 = require("../../common/transient-retry.js");
5
6
  const index_js_1 = require("../client/index.js");
6
7
  class SandboxDrive extends action_js_1.SandboxAction {
7
8
  constructor(sandbox) {
@@ -36,12 +37,16 @@ class SandboxDrive extends action_js_1.SandboxAction {
36
37
  * List all mounted drives in the sandbox
37
38
  */
38
39
  async list() {
39
- const { response, data, error } = await (0, index_js_1.getDrivesMount)(this.withClient({
40
- baseUrl: this.url,
41
- }));
42
- this.handleResponseError(response, data, error);
43
- const result = data;
44
- return result.mounts ?? [];
40
+ // Idempotent GET: self-heal a transient connection reset after sandbox resume.
41
+ // mount/unmount stay un-retried (non-idempotent POST/DELETE).
42
+ return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
43
+ const { response, data, error } = await (0, index_js_1.getDrivesMount)(this.withClient({
44
+ baseUrl: this.url,
45
+ }));
46
+ this.handleResponseError(response, data, error);
47
+ const result = data;
48
+ return result.mounts ?? [];
49
+ });
45
50
  }
46
51
  }
47
52
  exports.SandboxDrive = SandboxDrive;
@@ -1,101 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SandboxFileSystem = void 0;
3
+ exports.SandboxFileSystem = exports.isTransientUploadError = void 0;
4
+ exports.retryOnTransient = retryOnTransient;
4
5
  const node_js_1 = require("../../common/node.js");
5
6
  const settings_js_1 = require("../../common/settings.js");
7
+ const h2fetch_js_1 = require("../../common/h2fetch.js");
8
+ const transient_retry_js_1 = require("../../common/transient-retry.js");
6
9
  const action_js_1 = require("../action.js");
7
10
  const index_js_1 = require("../client/index.js");
8
11
  // Multipart upload constants
9
12
  const MULTIPART_THRESHOLD = 5 * 1024 * 1024; // 5MB
10
13
  const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB per part
11
14
  const MAX_PARALLEL_UPLOADS = 3; // Number of parallel part uploads
12
- // Base backoff between part-upload retries, in milliseconds. Grows linearly
13
- // per attempt and is jittered to avoid synchronized retries (thundering herd)
14
- // when several parallel parts fail against the same edge at the same time.
15
- const RETRY_BASE_DELAY_MS = 200;
16
- // Markers that, when present anywhere in the error chain, are unambiguous
17
- // signals of a transient HTTP/2 stream reset or connection drop. These are
18
- // protocol/transport level codes, not application payloads, so substring
19
- // matching them does not over-match a server-sent error body. Each entry is
20
- // matched case-sensitively against the error message and its cause.
21
- //
22
- // Deliberately excluded: bare "INTERNAL_ERROR" and "fetch failed". Both are
23
- // too generic on their own (an application 500 body or any failed fetch would
24
- // match), so we only treat them as transient when paired with a transport
25
- // error code on the cause (see isTransientUploadError).
26
- const TRANSIENT_RESET_MARKERS = [
27
- "ENHANCE_YOUR_CALM", // H2 flow-control backpressure reset
28
- "NGHTTP2_INTERNAL_ERROR", // H2 internal stream error (qualified form)
29
- "ERR_HTTP2", // node http2 error code family
30
- "GOAWAY", // peer is draining the connection
31
- "HTTP/2 session closed before response", // thrown by our own h2 transport
32
- "HTTP/2 session sent GOAWAY before response",
33
- ];
34
- // Node-level error codes (from `error.code` / `error.cause.code`) that mean
35
- // the connection itself dropped mid-flight and the request never completed.
36
- // These are safe to retry for an idempotent part upload.
37
- const TRANSIENT_ERROR_CODES = new Set([
38
- "ECONNRESET",
39
- "ECONNREFUSED",
40
- "ETIMEDOUT",
41
- "EPIPE",
42
- "ERR_HTTP2_STREAM_ERROR",
43
- "ERR_HTTP2_GOAWAY_SESSION",
44
- "ERR_HTTP2_SESSION_ERROR",
45
- ]);
46
- function collectErrorText(error) {
47
- const messages = [];
48
- const codes = [];
49
- // Walk the error -> cause chain (bounded) so a transport error wrapped by a
50
- // higher-level "fetch failed" is still classified correctly.
51
- let current = error;
52
- for (let depth = 0; depth < 5 && current && typeof current === "object"; depth++) {
53
- const node = current;
54
- if (typeof node.message === "string")
55
- messages.push(node.message);
56
- if (typeof node.code === "string")
57
- codes.push(node.code);
58
- current = node.cause;
59
- }
60
- return { messages, codes };
61
- }
62
- function isTransientUploadError(error) {
63
- if (!error || typeof error !== "object") {
64
- return false;
65
- }
66
- const { messages, codes } = collectErrorText(error);
67
- // 1. An explicit transient transport error code anywhere in the chain.
68
- if (codes.some((code) => TRANSIENT_ERROR_CODES.has(code))) {
69
- return true;
70
- }
71
- // 2. An unambiguous protocol-level reset marker in any message.
72
- if (messages.some((text) => TRANSIENT_RESET_MARKERS.some((marker) => text.includes(marker)))) {
73
- return true;
74
- }
75
- return false;
76
- }
77
- function nextRetryDelayMs(attempt) {
78
- // Linear backoff (200ms, 400ms, ...) plus up to one extra base delay of
79
- // random jitter so concurrent part retries do not all fire on the same tick.
80
- const base = RETRY_BASE_DELAY_MS * attempt;
81
- const jitter = Math.floor(Math.random() * RETRY_BASE_DELAY_MS);
82
- return base + jitter;
83
- }
84
- async function retryOnTransient(fn) {
85
- const retries = settings_js_1.settings.fsPartRetries; // 0 = off (current default behavior)
86
- let attempt = 0;
87
- for (;;) {
88
- try {
89
- return await fn();
90
- }
91
- catch (error) {
92
- attempt++;
93
- if (retries <= 0 || attempt > retries || !isTransientUploadError(error)) {
94
- throw error;
95
- }
96
- await new Promise((resolve) => setTimeout(resolve, nextRetryDelayMs(attempt)));
97
- }
98
- }
15
+ // The transient-reset classifier and retry loop live in
16
+ // common/transient-retry.ts, shared with the idempotent sandbox-op retry
17
+ // (read/list/etc.) so every path judges "transient" identically. These aliases
18
+ // preserve the import surface the ENG-2680 fault tests already depend on.
19
+ exports.isTransientUploadError = transient_retry_js_1.isTransientResetError;
20
+ function retryOnTransient(fn) {
21
+ // Upload path keeps its own (lower) fsPartRetries budget; the shared helper
22
+ // otherwise defaults to the higher idempotent-read budget (sandboxReadRetries).
23
+ return (0, transient_retry_js_1.retryOnTransientReset)(fn, { retries: settings_js_1.settings.fsPartRetries });
99
24
  }
100
25
  class SandboxFileSystem extends action_js_1.SandboxAction {
101
26
  process;
@@ -177,28 +102,44 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
177
102
  if (fileBlob.size > MULTIPART_THRESHOLD) {
178
103
  return await this.uploadWithMultipart(path, fileBlob, "0644");
179
104
  }
180
- // Use regular upload for small files
181
- const formData = new FormData();
182
- formData.append("file", fileBlob, "test-binary.bin");
183
- formData.append("permissions", "0644");
184
- formData.append("path", path);
185
- // Build URL
105
+ // Use regular upload for small files. Run the PUT under the same upload
106
+ // reliability wrapper as multipart parts: bound concurrency (ENG-2680) and
107
+ // retry transient connection resets (ECONNRESET/GOAWAY/ENHANCE_YOUR_CALM). A
108
+ // PUT of the same bytes to the same path is idempotent, so retry is safe.
109
+ // The FormData is rebuilt per attempt so a retried request has a fresh body.
186
110
  let url = `${this.url}/filesystem/${path}`;
187
111
  if (this.forcedUrl) {
188
112
  url = `${this.forcedUrl.toString()}/filesystem/${path}`;
189
113
  }
190
- const response = await this.h2Fetch(url, {
191
- method: 'PUT',
192
- headers: {
193
- ...settings_js_1.settings.headers,
194
- },
195
- body: formData,
196
- });
197
- if (!response.ok) {
198
- const errorText = await response.text();
199
- throw new Error(`Failed to write binary: ${response.status} ${errorText}`);
200
- }
201
- return await response.json();
114
+ const h2Domain = this.sandbox?.h2Domain;
115
+ const putOnce = async () => {
116
+ const formData = new FormData();
117
+ formData.append("file", fileBlob, "test-binary.bin");
118
+ formData.append("permissions", "0644");
119
+ formData.append("path", path);
120
+ // A forceUrl (session-token) sandbox carries its own headers and must not
121
+ // require global credentials; settings.headers now throws without them
122
+ // (ENG-2698). Mirror streamLogs/execWithStreaming, which already pick
123
+ // sandbox.headers for forceUrl sessions.
124
+ const headers = this.sandbox.forceUrl ? this.sandbox.headers : settings_js_1.settings.headers;
125
+ const response = await this.h2Fetch(url, {
126
+ method: 'PUT',
127
+ headers: {
128
+ ...headers,
129
+ },
130
+ body: formData,
131
+ });
132
+ if (!response.ok) {
133
+ const errorText = await response.text();
134
+ throw new Error(`Failed to write binary: ${response.status} ${errorText}`);
135
+ }
136
+ return await response.json();
137
+ };
138
+ // Acquire the upload slot per-attempt INSIDE the retry so the per-domain cap
139
+ // bounds in-flight streams, not retry sequences: the slot is released during
140
+ // backoff, so a failing PUT never pins a slot while it sleeps (Mendral review).
141
+ const putWithSlot = h2Domain ? () => (0, h2fetch_js_1.withUploadSlot)(h2Domain, putOnce) : putOnce;
142
+ return retryOnTransient(putWithSlot);
202
143
  }
203
144
  async writeTree(files, destinationPath = null) {
204
145
  const options = {
@@ -223,30 +164,37 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
223
164
  }
224
165
  async read(path) {
225
166
  path = this.formatPath(path);
226
- const { response, data, error } = await (0, index_js_1.getFilesystemByPath)(this.withClient({
227
- path: { path },
228
- baseUrl: this.url,
229
- }));
230
- this.handleResponseError(response, data, error);
231
- if (data && 'content' in data) {
232
- return data.content;
233
- }
234
- throw new Error("Unsupported file type");
167
+ // Idempotent GET: self-heal a transient connection reset (e.g. first call
168
+ // after sandbox resume). Non-transient errors (404, etc.) are not retried.
169
+ return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
170
+ const { response, data, error } = await (0, index_js_1.getFilesystemByPath)(this.withClient({
171
+ path: { path },
172
+ baseUrl: this.url,
173
+ }));
174
+ this.handleResponseError(response, data, error);
175
+ if (data && 'content' in data) {
176
+ return data.content;
177
+ }
178
+ throw new Error("Unsupported file type");
179
+ });
235
180
  }
236
181
  async readBinary(path) {
237
182
  path = this.formatPath(path);
238
- const { response, data, error } = await (0, index_js_1.getFilesystemByPath)(this.withClient({
239
- path: { path },
240
- baseUrl: this.url,
241
- headers: {
242
- 'Accept': 'application/octet-stream',
243
- },
244
- }));
245
- this.handleResponseError(response, data, error);
246
- if (typeof data === 'string') {
247
- return new Blob([data]);
248
- }
249
- return data;
183
+ // Idempotent GET: self-heal a transient connection reset.
184
+ return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
185
+ const { response, data, error } = await (0, index_js_1.getFilesystemByPath)(this.withClient({
186
+ path: { path },
187
+ baseUrl: this.url,
188
+ headers: {
189
+ 'Accept': 'application/octet-stream',
190
+ },
191
+ }));
192
+ this.handleResponseError(response, data, error);
193
+ if (typeof data === 'string') {
194
+ return new Blob([data]);
195
+ }
196
+ return data;
197
+ });
250
198
  }
251
199
  async download(src, destinationPath, { mode = 0o644 } = {}) {
252
200
  if (!node_js_1.fs) {
@@ -269,15 +217,19 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
269
217
  }
270
218
  async ls(path) {
271
219
  path = this.formatPath(path);
272
- const { response, data, error } = await (0, index_js_1.getFilesystemByPath)(this.withClient({
273
- path: { path },
274
- baseUrl: this.url,
275
- }));
276
- this.handleResponseError(response, data, error);
277
- if (!data || !('files' in data || 'subdirectories' in data)) {
278
- throw new Error(JSON.stringify({ error: "Directory not found" }));
279
- }
280
- return data;
220
+ // Idempotent GET: self-heal a transient connection reset (the file-tree
221
+ // refresh that customers hit as intermittent 500s after sandbox resume).
222
+ return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
223
+ const { response, data, error } = await (0, index_js_1.getFilesystemByPath)(this.withClient({
224
+ path: { path },
225
+ baseUrl: this.url,
226
+ }));
227
+ this.handleResponseError(response, data, error);
228
+ if (!data || !('files' in data || 'subdirectories' in data)) {
229
+ throw new Error(JSON.stringify({ error: "Directory not found" }));
230
+ }
231
+ return data;
232
+ });
281
233
  }
282
234
  async search(query, path = "/", options) {
283
235
  const formattedPath = this.formatPath(path);
@@ -294,13 +246,15 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
294
246
  if (options?.excludeHidden !== undefined) {
295
247
  queryParams.excludeHidden = options.excludeHidden;
296
248
  }
297
- const result = await (0, index_js_1.getFilesystemSearchByPath)(this.withClient({
298
- path: { path: formattedPath },
299
- query: queryParams,
300
- baseUrl: this.url,
301
- }));
302
- this.handleResponseError(result.response, result.data, result.error);
303
- return result.data;
249
+ return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
250
+ const result = await (0, index_js_1.getFilesystemSearchByPath)(this.withClient({
251
+ path: { path: formattedPath },
252
+ query: queryParams,
253
+ baseUrl: this.url,
254
+ }));
255
+ this.handleResponseError(result.response, result.data, result.error);
256
+ return result.data;
257
+ });
304
258
  }
305
259
  async find(path, options) {
306
260
  const formattedPath = this.formatPath(path);
@@ -320,13 +274,15 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
320
274
  if (options?.excludeHidden !== undefined) {
321
275
  queryParams.excludeHidden = options.excludeHidden;
322
276
  }
323
- const result = await (0, index_js_1.getFilesystemFindByPath)(this.withClient({
324
- path: { path: formattedPath },
325
- query: queryParams,
326
- baseUrl: this.url,
327
- }));
328
- this.handleResponseError(result.response, result.data, result.error);
329
- return result.data;
277
+ return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
278
+ const result = await (0, index_js_1.getFilesystemFindByPath)(this.withClient({
279
+ path: { path: formattedPath },
280
+ query: queryParams,
281
+ baseUrl: this.url,
282
+ }));
283
+ this.handleResponseError(result.response, result.data, result.error);
284
+ return result.data;
285
+ });
330
286
  }
331
287
  async grep(query, path = "/", options) {
332
288
  const formattedPath = this.formatPath(path);
@@ -348,13 +304,15 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
348
304
  if (options?.excludeDirs && options.excludeDirs.length > 0) {
349
305
  queryParams.excludeDirs = options.excludeDirs.join(',');
350
306
  }
351
- const result = await (0, index_js_1.getFilesystemContentSearchByPath)(this.withClient({
352
- path: { path: formattedPath },
353
- query: queryParams,
354
- baseUrl: this.url,
355
- }));
356
- this.handleResponseError(result.response, result.data, result.error);
357
- return result.data;
307
+ return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
308
+ const result = await (0, index_js_1.getFilesystemContentSearchByPath)(this.withClient({
309
+ path: { path: formattedPath },
310
+ query: queryParams,
311
+ baseUrl: this.url,
312
+ }));
313
+ this.handleResponseError(result.response, result.data, result.error);
314
+ return result.data;
315
+ });
358
316
  }
359
317
  async cp(source, destination, { maxWait = 180000 } = {}) {
360
318
  let process = await this.process.exec({
@@ -506,6 +464,12 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
506
464
  const size = blob.size;
507
465
  const numParts = Math.ceil(size / CHUNK_SIZE);
508
466
  const parts = [];
467
+ // Bound concurrent upload-part streams on the shared H2 connection so many
468
+ // parts (within and across files) cannot burst past the server's
469
+ // rapid-reset limit (ENG-2680). Scoped to uploads; default cap is 2. With
470
+ // no h2Domain the parts go over globalThis.fetch on separate connections,
471
+ // so the shared-connection cap does not apply.
472
+ const h2Domain = this.sandbox?.h2Domain;
509
473
  // Upload parts in batches for parallel processing
510
474
  for (let i = 0; i < numParts; i += MAX_PARALLEL_UPLOADS) {
511
475
  const batch = [];
@@ -514,7 +478,11 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
514
478
  const start = (partNumber - 1) * CHUNK_SIZE;
515
479
  const end = Math.min(start + CHUNK_SIZE, size);
516
480
  const chunk = blob.slice(start, end);
517
- batch.push(retryOnTransient(() => this.uploadPart(uploadId, partNumber, chunk)));
481
+ // Slot acquired per-attempt inside the retry (see writeBinary): bounds
482
+ // concurrent part streams, not retry sequences; freed during backoff.
483
+ const doPart = () => this.uploadPart(uploadId, partNumber, chunk);
484
+ const partWithSlot = h2Domain ? () => (0, h2fetch_js_1.withUploadSlot)(h2Domain, doPart) : doPart;
485
+ batch.push(retryOnTransient(partWithSlot));
518
486
  }
519
487
  // Wait for batch to complete
520
488
  const batchResults = await Promise.all(batch);