@prized/sdk 0.1.1 → 0.1.2

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.
@@ -10,6 +10,7 @@ exports.mayHaveActed = mayHaveActed;
10
10
  exports.restate = restate;
11
11
  exports.withOutcome = withOutcome;
12
12
  exports.withAbort = withAbort;
13
+ exports.startGaveUp = startGaveUp;
13
14
  exports.startAbandoned = startAbandoned;
14
15
  exports.runAborted = runAborted;
15
16
  exports.pollFailed = pollFailed;
@@ -105,12 +106,14 @@ function maybeTail(effect, closed) {
105
106
  tail += ` ${effect.check}`;
106
107
  return tail;
107
108
  }
109
+ /** How an error says its request never reached the sandbox (withOutcome). */
110
+ const NEVER_TAIL = ' It never reached the sandbox, so it had no effect.';
108
111
  /** `error` (same code, status and body), its message saying what it means for `effect`. */
109
112
  function withOutcome(error, effect, outcome) {
110
113
  if (outcome === null)
111
114
  return error;
112
115
  if (outcome === 'never')
113
- return restate(error, `${trimmed(error.message)}. It never reached the sandbox, so it had no effect.`);
116
+ return restate(error, `${trimmed(error.message)}.${NEVER_TAIL}`);
114
117
  if (outcome === 'did')
115
118
  return acting(error, `${effect.did}, but the answer could not be read (${detail(error)}).`);
116
119
  return acting(error, `No clear answer came back (${detail(error)}), so ${maybeTail(effect, false)}`);
@@ -134,6 +137,23 @@ function stopTail(runId, stopped) {
134
137
  ? ` The SDK stopped ${runId}, or it had already ended.`
135
138
  : ` The SDK could not confirm it stopped: it may still be running as ${runId} (exec('ps -ef | grep ${runId}') finds it).`;
136
139
  }
140
+ /**
141
+ * What a start that gave up throws when the failure it gives up with
142
+ * (`failure`, Outage's pick) is not the last copy's own: an earlier copy's,
143
+ * worded before a later copy may have reached the sandbox, or what a copy
144
+ * cut short by the window met, not yet worded. Worded for the start: as one
145
+ * that may have started when any copy may have reached the sandbox
146
+ * (`anyActed`), else as one that never did. The Python SDK's start_gave_up.
147
+ */
148
+ function startGaveUp(failure, anyActed) {
149
+ if (acted.has(failure))
150
+ return failure;
151
+ const never = failure.message.endsWith(NEVER_TAIL);
152
+ if (never && !anyActed)
153
+ return failure;
154
+ const plain = never ? restate(failure, failure.message.slice(0, -NEVER_TAIL.length)) : failure;
155
+ return withOutcome(plain, startEffect(), anyActed ? 'maybe' : 'never');
156
+ }
137
157
  /**
138
158
  * A background start that may have started (`error`, from withOutcome or
139
159
  * withAbort) and that the SDK then stopped by its run id (`stopped`: the
@@ -279,6 +279,7 @@ async function download(toolbox, path, signal) {
279
279
  retry: 'safe',
280
280
  read: 'bytes',
281
281
  timeoutMs: READ_MS,
282
+ stallMs: http_js_1.timing.transferStallMs,
282
283
  deadline: toolbox.config.now() + READ_MS,
283
284
  signal,
284
285
  sandboxId: toolbox.sandboxId,
@@ -128,8 +128,9 @@ export declare class Sandbox {
128
128
  /**
129
129
  * Delete every sandbox carrying all of `labels` (at least one) and resolve
130
130
  * with their ids (the API takes up to 250 a request; this repeats it until
131
- * none are left). Safe to repeat: sandboxes already being
132
- * deleted are in the answer again, and none matching resolves `[]`. Each
131
+ * none are left). Each request selects the current matches, so a repeat
132
+ * can delete sandboxes created since the first request. An uncertain
133
+ * answer is not retried; list the matches before deleting again. Each
133
134
  * counts toward the workspace's quotas until it is gone;
134
135
  * `Sandbox.list({ labels })` coming back empty means they all are.
135
136
  */
@@ -440,8 +440,9 @@ class Sandbox {
440
440
  /**
441
441
  * Delete every sandbox carrying all of `labels` (at least one) and resolve
442
442
  * with their ids (the API takes up to 250 a request; this repeats it until
443
- * none are left). Safe to repeat: sandboxes already being
444
- * deleted are in the answer again, and none matching resolves `[]`. Each
443
+ * none are left). Each request selects the current matches, so a repeat
444
+ * can delete sandboxes created since the first request. An uncertain
445
+ * answer is not retried; list the matches before deleting again. Each
445
446
  * counts toward the workspace's quotas until it is gone;
446
447
  * `Sandbox.list({ labels })` coming back empty means they all are.
447
448
  */
@@ -455,7 +456,15 @@ class Sandbox {
455
456
  const deleted = new Set();
456
457
  const stuck = (calls) => new errors_js_1.PrizedError('delete_incomplete', `Deleted ${deleted.size} sandboxes in ${calls} requests and some are left; call again to delete the rest.`);
457
458
  for (let call = 1; call <= MAX_BULK_DELETE_CALLS; call += 1) {
458
- const result = await (0, http_js_1.request)(config, { method: 'DELETE', url: apiUrl(config, `/sandboxes${query}`), retry: 'safe', deadline });
459
+ // A label is a live selector, not a stable resource or idempotency key.
460
+ const result = await (0, http_js_1.request)(config, {
461
+ method: 'DELETE', url: apiUrl(config, `/sandboxes${query}`), retry: 'exec', deadline,
462
+ effect: {
463
+ maybe: 'some matching sandboxes may already be deleting',
464
+ did: 'The matching sandboxes were marked for deletion',
465
+ check: 'It was not sent again: list the matching sandboxes before deleting again, because the matches may have changed.',
466
+ },
467
+ });
459
468
  const answer = result.json;
460
469
  const ids = answer?.ids;
461
470
  if (!Array.isArray(ids) || !ids.every((id) => typeof id === 'string')) {
@@ -11,7 +11,8 @@ export declare const RUN_ID_RE: RegExp;
11
11
  /**
12
12
  * Minutes (by the sandbox's clock) a cleaned or abandoned run's directory
13
13
  * stays: what keeps a resend of its start from running the command again.
14
- * The SDK stops resending a start within minutes, so an hour is ample; each
14
+ * The SDK stops resending a start once pollOutageMs (2 minutes by default)
15
+ * has run out after its first copy's retries, so an hour is ample; each
15
16
  * cleanup then removes the ones older than that, so they never pile up.
16
17
  */
17
18
  export declare const CLEANED_KEEP_MIN = 60;
package/dist/cjs/shell.js CHANGED
@@ -64,7 +64,8 @@ exports.RUN_ID_RE = /^run_[0-9a-f]{16}$/;
64
64
  /**
65
65
  * Minutes (by the sandbox's clock) a cleaned or abandoned run's directory
66
66
  * stays: what keeps a resend of its start from running the command again.
67
- * The SDK stops resending a start within minutes, so an hour is ample; each
67
+ * The SDK stops resending a start once pollOutageMs (2 minutes by default)
68
+ * has run out after its first copy's retries, so an hour is ample; each
68
69
  * cleanup then removes the ones older than that, so they never pile up.
69
70
  */
70
71
  exports.CLEANED_KEEP_MIN = 60;
@@ -30,6 +30,8 @@ export declare function rawExec(toolbox: Toolbox, req: RawExecRequest, options:
30
30
  deadline?: number | undefined;
31
31
  /** What the command does, for an error that says whether it happened (`exec`: a command unless given). */
32
32
  effect?: Effect | undefined;
33
+ /** An earlier copy of a keyed start may have reached the sandbox. */
34
+ acted?: boolean | undefined;
33
35
  }): Promise<ExecResult>;
34
36
  /** A helper command the SDK sends itself; a non-zero exit is the caller's to judge. */
35
37
  export declare function helperExec(toolbox: Toolbox, command: string, options?: {
@@ -51,6 +51,7 @@ async function rawExec(toolbox, req, options) {
51
51
  deadline: options.deadline,
52
52
  sandboxId: toolbox.sandboxId,
53
53
  ...(effect === undefined ? {} : { effect }),
54
+ ...(options.acted ? { acted: true } : {}),
54
55
  });
55
56
  const doc = result.json;
56
57
  if (doc === null || typeof doc !== 'object' || typeof doc.exitCode !== 'number') {
@@ -1,2 +1,2 @@
1
1
  /** This package's version; test/version.test.ts holds it equal to package.json. */
2
- export declare const VERSION = "0.1.1";
2
+ export declare const VERSION = "0.1.2";
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
4
  /** This package's version; test/version.test.ts holds it equal to package.json. */
5
- exports.VERSION = '0.1.1';
5
+ exports.VERSION = '0.1.2';
@@ -104,7 +104,9 @@ export interface ExecOptions {
104
104
  * its polls fail (no answer, a 5xx, a rate limit), in ms, before it gives
105
105
  * up and stops the command. Default: the client's `pollOutageMs`
106
106
  * (120 000). A refused key, or an answer that settles the run (the
107
- * sandbox deleted or not running, the run gone), stops it at once.
107
+ * sandbox deleted or not running, the run gone), stops it at once. The
108
+ * start of a background, streamed or long command is sent again for as
109
+ * long (a resend finds the first start, so the command runs once).
108
110
  */
109
111
  pollOutageMs?: number;
110
112
  }
@@ -3,7 +3,7 @@
3
3
  import { CommandTimeoutError, PrizedError } from './errors.js';
4
4
  import { backoffMs, cutByDeadline, LAST_LOOK_MAX_MS, MAX_RETRY_AFTER_MS, Polls, validOutage } from './http.js';
5
5
  import { abandonScript, cleanupScript, commandText, killScript, NO_PID_EXIT, pollScript, RUN_ID_RE, sandboxPath, SIGNALS, startScript, } from './shell.js';
6
- import { mayHaveActed, pollFailed, runAborted, signalEffect, startAbandoned, startEffect, withOutcome } from './outcome.js';
6
+ import { mayHaveActed, pollFailed, runAborted, signalEffect, startAbandoned, startEffect, startGaveUp, withAbort, withOutcome } from './outcome.js';
7
7
  import { execFullOutput } from './output.js';
8
8
  import { helperExec, rawExec } from './toolbox.js';
9
9
  /** The timeout a command gets when the call names none. */
@@ -470,7 +470,48 @@ async function cleanupOnceGone(toolbox, id) {
470
470
  }
471
471
  }
472
472
  }
473
- async function startBackground(toolbox, command, cwd, env, timeoutMs, signal, unbuffered) {
473
+ /**
474
+ * Send a start script. It is keyed by a run id the SDK chose, so a resend
475
+ * finds the first start instead of starting the command twice. Once one
476
+ * copy's retries run out, it is sent again through an outage of up to
477
+ * `outageMs` (pollOutageMs), paced as a wait's polls are (Outage): an edge
478
+ * that restarts mid-start does not cost the command, as it does not cost a
479
+ * running one its wait. Each copy's error counts the copies before it
480
+ * (`acted`), so one that gives up says the command may have started when any
481
+ * copy may have reached the sandbox. The Python SDK's _send_start.
482
+ */
483
+ async function sendStart(toolbox, req, signal, outageMs) {
484
+ const config = toolbox.config;
485
+ const outage = new Outage(outageMs);
486
+ // Whether an earlier copy may have reached the sandbox: an abort between
487
+ // copies then says the command may have started, else that it never did.
488
+ let acted = false;
489
+ const aborted = (cause) => withAbort(new PrizedError('aborted', null, { sandboxId: toolbox.sandboxId, cause }), startEffect(), acted ? 'maybe' : 'never');
490
+ for (;;) {
491
+ const sentAt = config.now();
492
+ try {
493
+ return await rawExec(toolbox, req, { retry: 'safe', signal, deadline: outage.deadline(sentAt), effect: startEffect(), acted });
494
+ }
495
+ catch (error) {
496
+ if (!(error instanceof PrizedError))
497
+ throw error;
498
+ acted = acted || mayHaveActed(error);
499
+ if (signal?.aborted)
500
+ throw error;
501
+ const step = outage.failed(error, sentAt, config.now(), cutByDeadline(error), (tries) => backoffMs(config, tries));
502
+ // What a copy met, not the window's cut.
503
+ if (step.pause === null)
504
+ throw step.failure === error ? error : startGaveUp(step.failure, acted);
505
+ try {
506
+ await config.sleep(step.pause, signal);
507
+ }
508
+ catch (abort) {
509
+ throw abort instanceof PrizedError && abort.code === 'aborted' ? aborted(step.failure) : abort;
510
+ }
511
+ }
512
+ }
513
+ }
514
+ async function startBackground(toolbox, command, cwd, env, timeoutMs, signal, unbuffered, outageMs) {
474
515
  // Nothing sent yet: nothing to stop.
475
516
  if (signal?.aborted)
476
517
  throw withOutcome(new PrizedError('aborted', null, { sandboxId: toolbox.sandboxId }), startEffect(), 'never');
@@ -482,10 +523,7 @@ async function startBackground(toolbox, command, cwd, env, timeoutMs, signal, un
482
523
  // Launched, but no pid came within the start's own wait: it may yet run.
483
524
  let unsettled = false;
484
525
  try {
485
- const result = await rawExec(toolbox, { command: startScript(id, command, timeoutMs === null ? null : timeoutMs / 1000, unbuffered), cwd, env, timeoutMs: 60_000 },
486
- // Keyed by a run id this call chose, so a resend finds the first start
487
- // instead of starting the command twice.
488
- { retry: 'safe', signal, effect: startEffect() });
526
+ const result = await sendStart(toolbox, { command: startScript(id, command, timeoutMs === null ? null : timeoutMs / 1000, unbuffered), cwd, env, timeoutMs: 60_000 }, signal, outageMs);
489
527
  pid = Number(result.stdout.trim());
490
528
  if (result.exitCode !== 0 || !Number.isInteger(pid) || pid <= 0) {
491
529
  unsettled = result.exitCode === NO_PID_EXIT;
@@ -531,7 +569,7 @@ export async function execCommand(toolbox, command, options = {}) {
531
569
  const cwd = options.cwd === undefined ? undefined : sandboxPath(options.cwd);
532
570
  if (options.background) {
533
571
  const limit = options.timeoutMs ? options.timeoutMs : null;
534
- const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, true);
572
+ const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, true, outageMs);
535
573
  handle.timeoutMs = limit;
536
574
  return handle;
537
575
  }
@@ -539,7 +577,7 @@ export async function execCommand(toolbox, command, options = {}) {
539
577
  const streamed = options.onStdout !== undefined || options.onStderr !== undefined;
540
578
  if (streamed || timeoutMs === 0 || timeoutMs > MAX_BLOCKING_EXEC_MS) {
541
579
  const limit = timeoutMs === 0 ? null : timeoutMs;
542
- const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, streamed);
580
+ const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, streamed, outageMs);
543
581
  const waitOptions = {};
544
582
  if (options.onStdout)
545
583
  waitOptions.onStdout = options.onStdout;
@@ -13,9 +13,10 @@ export interface WriteOptions {
13
13
  }
14
14
  /**
15
15
  * The sandbox's file transfers take at most this long, retries included:
16
- * the edge's own limit on one transfer. A write's upload sent again (a
17
- * staged one is, after a lost answer or a 502, 503 or 504) is sent only
18
- * while time is left, so a transfer that timed out is not.
16
+ * the edge's own limit on one transfer. An attempt that moves no bytes for
17
+ * timing.transferStallMs (2 minutes) is given up; a download, or a write's
18
+ * staged upload, is then sent again (as after a lost answer or a 502, 503 or
19
+ * 504) while time is left.
19
20
  */
20
21
  export declare const FILE_TIMEOUT_MS: number;
21
22
  /** The publish script's own exits (the Python SDK's too). */
@@ -60,8 +61,8 @@ export declare class Files {
60
61
  * stays executable; a new file gets the sandbox's default (0600 in
61
62
  * prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
62
63
  * The bytes go up under a temporary name beside the file, sent again if
63
- * need be (a lost answer, a 502, 503 or 504; all copies within the
64
- * transfer's 10 minutes),
64
+ * need be (a lost answer, a 502, 503 or 504, 2 minutes with no bytes
65
+ * moving; all copies within the transfer's 10 minutes),
65
66
  * and one command that is never sent again puts them in place: a write
66
67
  * whose answer to that command was lost throws, since it may have landed
67
68
  * and the sandbox written the file since (read it back, or write again,
package/dist/esm/files.js CHANGED
@@ -1,14 +1,15 @@
1
1
  // sandbox.files: read, write, list, remove, upload, download.
2
2
  import { PrizedError } from './errors.js';
3
- import { request } from './http.js';
3
+ import { request, timing } from './http.js';
4
4
  import { mayHaveActed, removeEffect, withOutcome, writeEffect } from './outcome.js';
5
5
  import { existsScript, listScript, removeScript, sandboxPath, shellQuote, WORKSPACE_DIR } from './shell.js';
6
6
  import { helperExec, MAX_FILE_BYTES, rawExec } from './toolbox.js';
7
7
  /**
8
8
  * The sandbox's file transfers take at most this long, retries included:
9
- * the edge's own limit on one transfer. A write's upload sent again (a
10
- * staged one is, after a lost answer or a 502, 503 or 504) is sent only
11
- * while time is left, so a transfer that timed out is not.
9
+ * the edge's own limit on one transfer. An attempt that moves no bytes for
10
+ * timing.transferStallMs (2 minutes) is given up; a download, or a write's
11
+ * staged upload, is then sent again (as after a lost answer or a 502, 503 or
12
+ * 504) while time is left.
12
13
  */
13
14
  export const FILE_TIMEOUT_MS = 10 * 60_000;
14
15
  function byteSize(data) {
@@ -163,6 +164,7 @@ export class Files {
163
164
  retry: 'safe',
164
165
  read: 'bytes',
165
166
  timeoutMs: FILE_TIMEOUT_MS,
167
+ stallMs: timing.transferStallMs,
166
168
  deadline: this.toolbox.config.now() + FILE_TIMEOUT_MS,
167
169
  sandboxId: this.toolbox.sandboxId,
168
170
  });
@@ -175,8 +177,8 @@ export class Files {
175
177
  * stays executable; a new file gets the sandbox's default (0600 in
176
178
  * prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
177
179
  * The bytes go up under a temporary name beside the file, sent again if
178
- * need be (a lost answer, a 502, 503 or 504; all copies within the
179
- * transfer's 10 minutes),
180
+ * need be (a lost answer, a 502, 503 or 504, 2 minutes with no bytes
181
+ * moving; all copies within the transfer's 10 minutes),
180
182
  * and one command that is never sent again puts them in place: a write
181
183
  * whose answer to that command was lost throws, since it may have landed
182
184
  * and the sandbox written the file since (read it back, or write again,
@@ -256,6 +258,7 @@ export class Files {
256
258
  retry,
257
259
  effect,
258
260
  timeoutMs: FILE_TIMEOUT_MS,
261
+ stallMs: timing.transferStallMs,
259
262
  deadline: this.toolbox.config.now() + FILE_TIMEOUT_MS,
260
263
  sandboxId: this.toolbox.sandboxId,
261
264
  });
@@ -32,7 +32,9 @@ export interface ClientOptions {
32
32
  * rate limit), in ms, before it gives up: a streamed or long `exec` then
33
33
  * stops the command, a handle's `wait()` throws. Default 120 000. `0`
34
34
  * gives up once one poll's retries run out. `exec` and `wait()` take it
35
- * per call too.
35
+ * per call too. The start of a background, streamed or long `exec` is
36
+ * sent again for as long, so an edge that restarts mid-start does not
37
+ * cost the command.
36
38
  */
37
39
  pollOutageMs?: number;
38
40
  /** Sleep seam (tests inject one that never really waits). */
@@ -54,13 +56,21 @@ export interface Config {
54
56
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
55
57
  now: () => number;
56
58
  random: () => number;
59
+ /**
60
+ * Whether an upload's body goes out as a stream the SDK feeds piece by
61
+ * piece, so a stalled one is seen (RequestSpec.stallMs): Node's own fetch
62
+ * only, which asks for each piece once the last has gone into the socket.
63
+ * Elsewhere (another runtime, a fetch you pass) the body goes as it is.
64
+ */
65
+ streamBodies: boolean;
57
66
  }
58
67
  export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
59
68
  /**
60
69
  * How long a wait (a streamed or long exec, a handle's wait()) keeps polling
61
70
  * while its polls fail before it gives up (pollOutageMs), and a streamed or
62
- * long exec stops its command: long enough to ride out a dropped connection
63
- * or an edge that restarts. The Python SDK's DEFAULT_POLL_OUTAGE.
71
+ * long exec stops its command, and how long a start is sent again: long
72
+ * enough to ride out a dropped connection or an edge that restarts (about
73
+ * 70 s, the last time it crashed). The Python SDK's DEFAULT_POLL_OUTAGE.
64
74
  */
65
75
  export declare const DEFAULT_POLL_OUTAGE_MS = 120000;
66
76
  /** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
@@ -97,8 +107,8 @@ export declare function warmEdge(config: Config): Promise<void>;
97
107
  * Retries network failures, 429/502/503/504, and everything `exec`
98
108
  * retries (the edge's 500 control_plane_unavailable among them). A GET
99
109
  * retries any 500 too (the API's `internal`).
100
- * - `exec`: a command, an upload or a remove: what the sandbox does
101
- * meanwhile could be undone by a second copy. Retried only when it
110
+ * - `exec`: a command, an upload, a remove or a label-selected delete:
111
+ * a second copy could affect later work. Retried only when it
102
112
  * provably never arrived: a connection that never opened, or an answer
103
113
  * saying so (the edge's rate limit, its lookup failing). A lost answer
104
114
  * is never retried: it may already have run.
@@ -139,6 +149,14 @@ export interface RequestSpec {
139
149
  sandboxId?: string | null;
140
150
  /** How to read a 2xx body. Default `json`. */
141
151
  read?: 'json' | 'bytes' | 'none';
152
+ /**
153
+ * A file transfer's no-progress bound: an attempt that moves no bytes
154
+ * either way for this long (a connection dropped without a reset) ends as
155
+ * a timeout, sent again by `retry`'s rules while `deadline` allows. An
156
+ * upload's sending is watched only where its body is streamed
157
+ * (Config.streamBodies); its answer always is.
158
+ */
159
+ stallMs?: number;
142
160
  /**
143
161
  * Told of every attempt that failed (no answer, an error answer, or a 2xx
144
162
  * that could not be read), before it is retried or thrown. A create or
@@ -153,6 +171,12 @@ export interface RequestSpec {
153
171
  * its error says so too.
154
172
  */
155
173
  effect?: Effect;
174
+ /**
175
+ * An earlier copy of this keyed request may have reached the sandbox (the
176
+ * background start, sent again through an outage), so its error says so
177
+ * too.
178
+ */
179
+ acted?: boolean;
156
180
  }
157
181
  export interface HttpResult {
158
182
  status: number;
@@ -215,6 +239,25 @@ export declare class Polls {
215
239
  }
216
240
  /** Delay before retry number `attempt` (0-based): exponential with jitter. */
217
241
  export declare function backoffMs(config: Config, attempt: number): number;
242
+ /**
243
+ * How long a file transfer may move no bytes before the attempt is given up
244
+ * (sdk/retry-spec.json transferStallMs; the Python SDK's TRANSFER_STALL).
245
+ * The edge holds a request about 20 s at most before it reads the body (its
246
+ * admission queue, then waking a paused sandbox), and ends a body that brings
247
+ * nothing for 60 s itself: kept above that, a stalled copy is over at the
248
+ * edge before the next one goes to the same staged name, so the two never
249
+ * race. Once an upload's last byte is handed over, its answer is waited for
250
+ * that and a second more for every uploadAnswerBytesPerSecond it sent: what
251
+ * the socket's buffers, or a proxy that takes a whole body before it sends it
252
+ * on, still hold moves unseen, and must not read as a stall (the Python SDK's
253
+ * UPLOAD_ANSWER_RATE). Mutable for tests only.
254
+ */
255
+ export declare const timing: {
256
+ transferStallMs: number;
257
+ uploadAnswerBytesPerSecond: number;
258
+ };
259
+ /** A streamed upload goes out this many bytes at a time. */
260
+ export declare const UPLOAD_CHUNK: number;
218
261
  /**
219
262
  * What an error answer (not a 2xx: request() judges those) says about
220
263
  * whether the sandbox acted: `never` for the codes that prove the request