@prized/sdk 0.1.0 → 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.
Files changed (48) hide show
  1. package/README.md +76 -21
  2. package/dist/cjs/commands.d.ts +47 -14
  3. package/dist/cjs/commands.js +94 -26
  4. package/dist/cjs/errors.d.ts +43 -3
  5. package/dist/cjs/errors.js +54 -6
  6. package/dist/cjs/files.d.ts +21 -5
  7. package/dist/cjs/files.js +38 -14
  8. package/dist/cjs/http.d.ts +74 -15
  9. package/dist/cjs/http.js +208 -32
  10. package/dist/cjs/index.d.ts +3 -3
  11. package/dist/cjs/index.js +2 -1
  12. package/dist/cjs/outcome.d.ts +11 -1
  13. package/dist/cjs/outcome.js +28 -5
  14. package/dist/cjs/output.d.ts +19 -8
  15. package/dist/cjs/output.js +152 -56
  16. package/dist/cjs/sandbox.d.ts +3 -2
  17. package/dist/cjs/sandbox.js +40 -5
  18. package/dist/cjs/shell.d.ts +2 -1
  19. package/dist/cjs/shell.js +2 -1
  20. package/dist/cjs/toolbox.d.ts +2 -0
  21. package/dist/cjs/toolbox.js +4 -1
  22. package/dist/cjs/types.d.ts +13 -1
  23. package/dist/cjs/version.d.ts +1 -1
  24. package/dist/cjs/version.js +1 -1
  25. package/dist/esm/commands.d.ts +47 -14
  26. package/dist/esm/commands.js +95 -27
  27. package/dist/esm/errors.d.ts +43 -3
  28. package/dist/esm/errors.js +52 -5
  29. package/dist/esm/files.d.ts +21 -5
  30. package/dist/esm/files.js +39 -15
  31. package/dist/esm/http.d.ts +74 -15
  32. package/dist/esm/http.js +207 -31
  33. package/dist/esm/index.d.ts +3 -3
  34. package/dist/esm/index.js +1 -1
  35. package/dist/esm/outcome.d.ts +11 -1
  36. package/dist/esm/outcome.js +27 -5
  37. package/dist/esm/output.d.ts +19 -8
  38. package/dist/esm/output.js +153 -58
  39. package/dist/esm/sandbox.d.ts +3 -2
  40. package/dist/esm/sandbox.js +40 -5
  41. package/dist/esm/shell.d.ts +2 -1
  42. package/dist/esm/shell.js +2 -1
  43. package/dist/esm/toolbox.d.ts +2 -0
  44. package/dist/esm/toolbox.js +4 -1
  45. package/dist/esm/types.d.ts +13 -1
  46. package/dist/esm/version.d.ts +1 -1
  47. package/dist/esm/version.js +1 -1
  48. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CommandTimeoutError = exports.PrizedError = exports.FREE_TIER_URL = exports.SIGN_UP_URL = exports.API_KEYS_URL = void 0;
3
+ exports.OutputReadError = exports.CommandTimeoutError = exports.PrizedError = exports.FREE_TIER_URL = exports.AGENT_SIGN_UP_URL = exports.SIGN_UP_URL = exports.API_KEYS_URL = void 0;
4
4
  exports.defaultMessage = defaultMessage;
5
5
  exports.parseRetryAfter = parseRetryAfter;
6
6
  exports.errorFromResponse = errorFromResponse;
@@ -8,15 +8,22 @@ exports.errorFromResponse = errorFromResponse;
8
8
  exports.API_KEYS_URL = 'https://prized.dev/dashboard/workspace/api-keys';
9
9
  /** Where a newcomer signs up (free credit, no card) and gets one. */
10
10
  exports.SIGN_UP_URL = 'https://prized.dev/onboarding/sandboxes';
11
+ /** What a coding agent follows to sign its person up and get a key itself:
12
+ * their email address, then the 6-digit code Prized emails them. The SDK's
13
+ * own key errors name it; a refused key's 401 carries the API's own line
14
+ * (`signUp`), which follows the deployment. */
15
+ exports.AGENT_SIGN_UP_URL = 'https://prized.dev/auth.md';
16
+ const AGENT_SIGN_UP = `No Prized account yet? If you are an agent, you can sign your person up with their email and a 6-digit code: follow ${exports.AGENT_SIGN_UP_URL}`;
11
17
  /** The free tier's limits, and what lifts them. */
12
18
  exports.FREE_TIER_URL = 'https://prized.dev/docs/sandboxes#free-tier';
13
19
  /** A sentence for the codes a caller is most likely to meet, used when the
14
20
  * answer carries no `message` of its own. */
15
21
  const MESSAGES = {
16
22
  // The SDK's own codes.
17
- missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${exports.SIGN_UP_URL} signs you up and makes one; otherwise make one at ${exports.API_KEYS_URL}.`,
18
- invalid_api_key: `The API key has characters no key has (the docs' placeholder, or a stray character?); copy it again from ${exports.API_KEYS_URL}.`,
23
+ missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${exports.SIGN_UP_URL} signs you up and makes one, or, if you are an agent, you can sign your person up with their email and a 6-digit code: follow ${exports.AGENT_SIGN_UP_URL}, keep the key file it writes, and pass that key as apiKey at run time, never in source. Otherwise make one at ${exports.API_KEYS_URL}.`,
24
+ invalid_api_key: `The API key has characters no key has (the docs' placeholder, or a stray character?); copy it again from ${exports.API_KEYS_URL}. ${AGENT_SIGN_UP}`,
19
25
  invalid_argument: 'An argument was not valid.',
26
+ edge_unknown: 'The API named no command endpoint this SDK trusts; pass edgeUrl for that deployment.',
20
27
  network_error: 'Could not reach the Prized API.',
21
28
  timeout: 'Timed out.',
22
29
  aborted: 'The operation was aborted.',
@@ -27,6 +34,7 @@ const MESSAGES = {
27
34
  sandbox_deleted: 'The sandbox was deleted.',
28
35
  command_failed: 'The command failed in the sandbox.',
29
36
  command_timeout: 'The command ran past its timeout and was killed.',
37
+ output_read_failed: 'The command ran, but its output could not be read back from the sandbox.',
30
38
  not_directory: 'Not a directory.',
31
39
  // The API's and the exec edge's codes.
32
40
  unauthorized: `The API key was refused. Check that it is a live key from ${exports.API_KEYS_URL}.`,
@@ -83,13 +91,21 @@ function defaultMessage(code) {
83
91
  * - `code`: the API's error string, e.g. `paid_balance_required`,
84
92
  * `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
85
93
  * `rate_limited`; or one of the SDK's own: `missing_api_key`,
86
- * `invalid_api_key`, `invalid_argument`, `network_error`, `timeout`, `aborted`,
94
+ * `invalid_api_key`, `invalid_argument`, `edge_unknown` (another
95
+ * deployment's API named no command endpoint this SDK sends the key to),
96
+ * `network_error`, `timeout`, `aborted`,
87
97
  * `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
88
- * `command_failed`, `command_timeout` (thrown as CommandTimeoutError).
98
+ * `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
99
+ * `output_read_failed` (thrown as OutputReadError).
89
100
  * - `reason`: the finer-grained reason, when there is one (for
90
101
  * `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
91
102
  * `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
92
- * `max_runtime`).
103
+ * `max_runtime`; for `output_read_failed`, the code of the failure that
104
+ * stopped the read).
105
+ * - `exitCode`: the command's exit code when the error came after it ran: a
106
+ * `fullOutput` command whose output could not be read back
107
+ * (`output_read_failed`, or `sandbox_deleted` when the sandbox was deleted
108
+ * first). Don't run such a command again blindly. Null otherwise.
93
109
  */
94
110
  class PrizedError extends Error {
95
111
  status;
@@ -98,6 +114,7 @@ class PrizedError extends Error {
98
114
  sandboxId;
99
115
  body;
100
116
  retryAfter;
117
+ exitCode;
101
118
  constructor(code, message, init = {}) {
102
119
  super(message || defaultMessage(code), init.cause === undefined ? undefined : { cause: init.cause });
103
120
  this.name = 'PrizedError';
@@ -107,6 +124,7 @@ class PrizedError extends Error {
107
124
  this.sandboxId = init.sandboxId ?? null;
108
125
  this.body = init.body ?? null;
109
126
  this.retryAfter = init.retryAfter ?? null;
127
+ this.exitCode = init.exitCode ?? null;
110
128
  }
111
129
  toString() {
112
130
  const head = this.status === null ? this.code : `${this.status} ${this.code}`;
@@ -143,6 +161,31 @@ class CommandTimeoutError extends PrizedError {
143
161
  }
144
162
  }
145
163
  exports.CommandTimeoutError = CommandTimeoutError;
164
+ /**
165
+ * A `fullOutput` command ran, but its output could not be read back from the
166
+ * files it was copied to (`code` is `output_read_failed`). The command did
167
+ * run (`exitCode` is its exit code), so do not run it again blindly; and the
168
+ * sandbox is not known to be gone (a deleted one throws `sandbox_deleted`
169
+ * instead). `reason` is the code of the failure that stopped the read (say
170
+ * `not_found`, `network_error`), and `cause` is that error, with its HTTP
171
+ * status; `status` is null, as the command's own call succeeded. The output
172
+ * stays in the sandbox for an hour at `stdoutPath` and `stderrPath`: read it
173
+ * with files.read(), then remove their directory.
174
+ */
175
+ class OutputReadError extends PrizedError {
176
+ exitCode;
177
+ stdoutPath;
178
+ stderrPath;
179
+ constructor(message, init) {
180
+ const { cause } = init;
181
+ super('output_read_failed', message, { reason: cause.code, sandboxId: cause.sandboxId, cause, exitCode: init.exitCode });
182
+ this.name = 'OutputReadError';
183
+ this.exitCode = init.exitCode;
184
+ this.stdoutPath = init.stdoutPath;
185
+ this.stderrPath = init.stderrPath;
186
+ }
187
+ }
188
+ exports.OutputReadError = OutputReadError;
146
189
  function stringField(record, key) {
147
190
  const value = record[key];
148
191
  return typeof value === 'string' && value !== '' ? value : null;
@@ -254,5 +297,10 @@ function errorFromResponse(status, text, retryAfter, sandboxId, path = '') {
254
297
  const reason = doc ? stringField(doc, 'reason') ?? stringField(doc, 'field') ?? stringField(doc, 'detail') : null;
255
298
  if (doc && doc.tier === 'free' && !(message ?? '').includes('free tier'))
256
299
  message = freeTierMessage(code, message, reason, doc.limit);
300
+ // A refused key's 401 may say how an agent signs its person up (the API
301
+ // sends it where agent sign-in answers, with that deployment's address).
302
+ const signUp = code === 'unauthorized' && doc ? stringField(doc, 'signUp') : null;
303
+ if (signUp !== null)
304
+ message = `${message ?? defaultMessage(code)} ${signUp.slice(0, 300)}`;
257
305
  return new PrizedError(code, message, { status, reason, sandboxId, body: doc, retryAfter });
258
306
  }
@@ -11,7 +11,13 @@ export interface WriteOptions {
11
11
  */
12
12
  mode?: number;
13
13
  }
14
- /** The sandbox's file transfers take at most this long, retries included. */
14
+ /**
15
+ * The sandbox's file transfers take at most this long, retries included:
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.
20
+ */
15
21
  export declare const FILE_TIMEOUT_MS: number;
16
22
  /** The publish script's own exits (the Python SDK's too). */
17
23
  export declare const EXIT_TARGET_IS_LINK = 47;
@@ -54,12 +60,22 @@ export declare class Files {
54
60
  * A file that is replaced keeps its mode, so a script made executable
55
61
  * stays executable; a new file gets the sandbox's default (0600 in
56
62
  * prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
57
- * Sent again only if it provably never arrived: a write whose answer was
58
- * lost throws, since it may have landed and the sandbox written the file
59
- * since (read it back, or write again, as suits the file).
63
+ * The bytes go up under a temporary name beside the file, sent again if
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),
66
+ * and one command that is never sent again puts them in place: a write
67
+ * whose answer to that command was lost throws, since it may have landed
68
+ * and the sandbox written the file since (read it back, or write again,
69
+ * as suits the file).
60
70
  */
61
71
  write(path: string, data: FileData, options?: WriteOptions): Promise<WriteInfo>;
62
- /** One upload to `path`, as the files API takes it; `target` is the file the caller asked for, and `effect` what the upload does to it. */
72
+ /**
73
+ * One upload to `path`, as the files API takes it; `target` is the file the
74
+ * caller asked for, and `effect` what the upload does to it. `exec` for an
75
+ * upload straight to the file: like a command, a resend after the answer
76
+ * was lost could overwrite what the sandbox wrote to the path since.
77
+ * `staged` for one to a fresh name: sent again like a read.
78
+ */
63
79
  private send;
64
80
  /**
65
81
  * Remove a staged upload that was not put in place (best effort: the
package/dist/cjs/files.js CHANGED
@@ -9,7 +9,13 @@ const http_js_1 = require("./http.js");
9
9
  const outcome_js_1 = require("./outcome.js");
10
10
  const shell_js_1 = require("./shell.js");
11
11
  const toolbox_js_1 = require("./toolbox.js");
12
- /** The sandbox's file transfers take at most this long, retries included. */
12
+ /**
13
+ * The sandbox's file transfers take at most this long, retries included:
14
+ * the edge's own limit on one transfer. An attempt that moves no bytes for
15
+ * timing.transferStallMs (2 minutes) is given up; a download, or a write's
16
+ * staged upload, is then sent again (as after a lost answer or a 502, 503 or
17
+ * 504) while time is left.
18
+ */
13
19
  exports.FILE_TIMEOUT_MS = 10 * 60_000;
14
20
  function byteSize(data) {
15
21
  if (typeof data === 'string')
@@ -45,12 +51,16 @@ exports.EXIT_TARGET_NOT_REGULAR = 48;
45
51
  const DISCARD_MS = 10_000;
46
52
  /** A write's own commands: a rename, a chmod or an rm. */
47
53
  const WRITE_COMMAND_MS = 60_000;
48
- /** For the staged upload: the target is changed only by the command that follows it. */
54
+ /**
55
+ * For the staged upload: the target is changed only by the command that
56
+ * follows it. The upload may have been sent again, so it says only what to
57
+ * do (langchain-prized's staged_upload_effect words it the same).
58
+ */
49
59
  function stagedEffect(path) {
50
60
  return {
51
61
  maybe: `${path} was not changed (its new bytes may have reached the sandbox, but were not put in place)`,
52
62
  did: `${path} was not changed yet`,
53
- check: 'It was not sent again: write it again.',
63
+ check: 'Write it again.',
54
64
  };
55
65
  }
56
66
  /** For the command that puts the staged upload in place: a 2xx proves it ran, not how it ended. */
@@ -159,6 +169,7 @@ class Files {
159
169
  retry: 'safe',
160
170
  read: 'bytes',
161
171
  timeoutMs: exports.FILE_TIMEOUT_MS,
172
+ stallMs: http_js_1.timing.transferStallMs,
162
173
  deadline: this.toolbox.config.now() + exports.FILE_TIMEOUT_MS,
163
174
  sandboxId: this.toolbox.sandboxId,
164
175
  });
@@ -170,9 +181,13 @@ class Files {
170
181
  * A file that is replaced keeps its mode, so a script made executable
171
182
  * stays executable; a new file gets the sandbox's default (0600 in
172
183
  * prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
173
- * Sent again only if it provably never arrived: a write whose answer was
174
- * lost throws, since it may have landed and the sandbox written the file
175
- * since (read it back, or write again, as suits the file).
184
+ * The bytes go up under a temporary name beside the file, sent again if
185
+ * need be (a lost answer, a 502, 503 or 504, 2 minutes with no bytes
186
+ * moving; all copies within the transfer's 10 minutes),
187
+ * and one command that is never sent again puts them in place: a write
188
+ * whose answer to that command was lost throws, since it may have landed
189
+ * and the sandbox written the file since (read it back, or write again,
190
+ * as suits the file).
176
191
  */
177
192
  async write(path, data, options = {}) {
178
193
  const sandboxId = this.toolbox.sandboxId;
@@ -184,14 +199,18 @@ class Files {
184
199
  const target = (0, shell_js_1.sandboxPath)(path);
185
200
  const plan = stagingFor(target);
186
201
  if (plan === null)
187
- return this.send(target, data, size, target, (0, outcome_js_1.writeEffect)(target));
202
+ return this.send(target, data, size, target, (0, outcome_js_1.writeEffect)(target), 'exec');
188
203
  const { clean, staging } = plan;
189
204
  try {
190
- await this.send(staging, data, size, clean, stagedEffect(clean));
205
+ // Every copy to the same fresh name, which only the command below puts in place.
206
+ await this.send(staging, data, size, clean, stagedEffect(clean), 'staged');
191
207
  }
192
208
  catch (error) {
193
209
  // A 2xx whose body could not be read: the staged bytes are all there.
194
210
  if (!landed(error)) {
211
+ // Removed only if a copy may have landed: a refusal, or copies that
212
+ // never arrived, left nothing, and a command would wake a sandbox
213
+ // the edge's refusal left paused.
195
214
  if ((0, outcome_js_1.mayHaveActed)(error))
196
215
  await this.discard(staging);
197
216
  throw error;
@@ -217,7 +236,7 @@ class Files {
217
236
  return { path: clean, size };
218
237
  if (result.exitCode === exports.EXIT_TARGET_IS_LINK) {
219
238
  // A link: the files API follows it or refuses it, as for any upload.
220
- const info = await this.send(clean, data, size, clean, (0, outcome_js_1.writeEffect)(clean));
239
+ const info = await this.send(clean, data, size, clean, (0, outcome_js_1.writeEffect)(clean), 'exec');
221
240
  if (mode !== null)
222
241
  await this.chmod(clean, mode);
223
242
  return info;
@@ -228,18 +247,23 @@ class Files {
228
247
  const denied = /Permission denied|Operation not permitted/.test(result.stderr);
229
248
  throw new errors_js_1.PrizedError(denied ? 'permission_denied' : 'command_failed', `Could not write ${clean}: ${commandDetail(result)}`, { sandboxId });
230
249
  }
231
- /** One upload to `path`, as the files API takes it; `target` is the file the caller asked for, and `effect` what the upload does to it. */
232
- async send(path, data, size, target, effect) {
250
+ /**
251
+ * One upload to `path`, as the files API takes it; `target` is the file the
252
+ * caller asked for, and `effect` what the upload does to it. `exec` for an
253
+ * upload straight to the file: like a command, a resend after the answer
254
+ * was lost could overwrite what the sandbox wrote to the path since.
255
+ * `staged` for one to a fresh name: sent again like a read.
256
+ */
257
+ async send(path, data, size, target, effect, retry) {
233
258
  const result = await (0, http_js_1.request)(this.toolbox.config, {
234
259
  method: 'POST',
235
260
  url: this.url('upload', path),
236
261
  body: data,
237
262
  contentType: 'application/octet-stream',
238
- // Like a command: a resend after the answer was lost could overwrite
239
- // what the sandbox wrote to the path since.
240
- retry: 'exec',
263
+ retry,
241
264
  effect,
242
265
  timeoutMs: exports.FILE_TIMEOUT_MS,
266
+ stallMs: http_js_1.timing.transferStallMs,
243
267
  deadline: this.toolbox.config.now() + exports.FILE_TIMEOUT_MS,
244
268
  sandboxId: this.toolbox.sandboxId,
245
269
  });
@@ -6,11 +6,16 @@ export declare const DEFAULT_EDGE_URL = "https://term.prized.dev";
6
6
  export interface ClientOptions {
7
7
  /** Your API key (`dcp_…`). Default: the `PRIZED_API_KEY` environment variable. */
8
8
  apiKey?: string;
9
- /** The API origin. Default: `https://api.prized.dev`. */
9
+ /**
10
+ * The API origin: https, or plain http only on this machine (`localhost`,
11
+ * `127.0.0.1`, `[::1]`). Default: `https://api.prized.dev`.
12
+ */
10
13
  baseUrl?: string;
11
14
  /**
12
- * The origin that serves exec and files. Default: the `toolboxBase` the API
13
- * returns for each sandbox (today `https://term.prized.dev`).
15
+ * The origin that serves exec and files, by the same rule as `baseUrl`.
16
+ * Default: the `toolboxBase` the API returns for each sandbox (today
17
+ * `https://term.prized.dev`), on a host trusted beside the API's
18
+ * (toolboxBaseFor).
14
19
  */
15
20
  edgeUrl?: string;
16
21
  /** A fetch implementation. Default: the global `fetch`. */
@@ -23,10 +28,13 @@ export interface ClientOptions {
23
28
  * the request itself and the API's hold of a create, pause or resume. Default 500. */
24
29
  pollIntervalMs?: number;
25
30
  /**
26
- * How long a streamed or long `exec` keeps polling while its polls fail
27
- * (no answer, a 5xx, a rate limit), in ms, before it gives up and stops
28
- * the command. Default 120 000. `0` gives up once one poll's retries run
29
- * out. `exec` takes it per call too.
31
+ * How long a wait keeps polling while its polls fail (no answer, a 5xx, a
32
+ * rate limit), in ms, before it gives up: a streamed or long `exec` then
33
+ * stops the command, a handle's `wait()` throws. Default 120 000. `0`
34
+ * gives up once one poll's retries run out. `exec` and `wait()` take it
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.
30
38
  */
31
39
  pollOutageMs?: number;
32
40
  /** Sleep seam (tests inject one that never really waits). */
@@ -48,13 +56,21 @@ export interface Config {
48
56
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
49
57
  now: () => number;
50
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;
51
66
  }
52
67
  export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
53
68
  /**
54
- * How long a streamed or long exec keeps polling while its polls fail
55
- * before it stops the command (pollOutageMs): long enough to ride out a
56
- * dropped connection or an edge that restarts. The Python SDK's
57
- * DEFAULT_POLL_OUTAGE.
69
+ * How long a wait (a streamed or long exec, a handle's wait()) keeps polling
70
+ * while its polls fail before it gives up (pollOutageMs), and a streamed or
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.
58
74
  */
59
75
  export declare const DEFAULT_POLL_OUTAGE_MS = 120000;
60
76
  /** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
@@ -63,7 +79,10 @@ export declare function resolveConfig(options?: ClientOptions): Config;
63
79
  /**
64
80
  * Where exec and files go for one sandbox. An explicit edgeUrl wins; else the
65
81
  * API's own `toolboxBase` when it has the expected shape (https, or plain
66
- * http on loopback for local development); else the default edge.
82
+ * http on loopback for local development, ending /v1/sandbox/<id>) on a host
83
+ * trusted beside the API's (term.prized.dev beside api.prized.dev; see
84
+ * trustedEdge); else the default edge, for Prized's own API only. The API key
85
+ * is never sent anywhere else: discovery is routing, not authority.
67
86
  */
68
87
  export declare function toolboxBaseFor(config: Config, sandboxId: string, toolboxBase: string | null): string;
69
88
  /**
@@ -88,13 +107,20 @@ export declare function warmEdge(config: Config): Promise<void>;
88
107
  * Retries network failures, 429/502/503/504, and everything `exec`
89
108
  * retries (the edge's 500 control_plane_unavailable among them). A GET
90
109
  * retries any 500 too (the API's `internal`).
91
- * - `exec`: a command, an upload or a remove: what the sandbox does
92
- * 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
93
112
  * provably never arrived: a connection that never opened, or an answer
94
113
  * saying so (the edge's rate limit, its lookup failing). A lost answer
95
114
  * is never retried: it may already have run.
115
+ * - `staged`: a write's upload to a fresh name beside its target, which only
116
+ * a command after it puts in place (files.ts). Retried as `safe` (a lost
117
+ * answer, a timeout, 429/502/503/504) and `exec` both, a GET's 500 aside:
118
+ * every copy writes the same bytes under the same name, which nothing else
119
+ * writes, so a second one changes no file of the caller's. A 2xx whose body
120
+ * broke off is not sent again: the bytes are there. Its error says whether
121
+ * a copy may have reached the sandbox.
96
122
  */
97
- export type RetryMode = 'safe' | 'exec';
123
+ export type RetryMode = 'safe' | 'exec' | 'staged';
98
124
  /**
99
125
  * Whether a fetch that threw never sent its request: the connection was
100
126
  * refused, the name did not resolve, or connecting timed out. Anything else
@@ -123,6 +149,14 @@ export interface RequestSpec {
123
149
  sandboxId?: string | null;
124
150
  /** How to read a 2xx body. Default `json`. */
125
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;
126
160
  /**
127
161
  * Told of every attempt that failed (no answer, an error answer, or a 2xx
128
162
  * that could not be read), before it is retried or thrown. A create or
@@ -137,6 +171,12 @@ export interface RequestSpec {
137
171
  * its error says so too.
138
172
  */
139
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;
140
180
  }
141
181
  export interface HttpResult {
142
182
  status: number;
@@ -199,6 +239,25 @@ export declare class Polls {
199
239
  }
200
240
  /** Delay before retry number `attempt` (0-based): exponential with jitter. */
201
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;
202
261
  /**
203
262
  * What an error answer (not a 2xx: request() judges those) says about
204
263
  * whether the sandbox acted: `never` for the codes that prove the request