@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
@@ -19,20 +19,34 @@ export declare const MAX_BLOCKING_EXEC_MS = 240000;
19
19
  export declare function ridesOut(error: PrizedError): boolean;
20
20
  /**
21
21
  * The failed polls of a wait that rides out an outage of up to `windowMs`
22
- * (exec's pollOutageMs), from when the first poll's retries ran out: polled
23
- * again with backoff, each poll and its retries ending before the last look,
24
- * which goes out LAST_LOOK_MAX_MS before the window's end (half the window,
25
- * for one shorter than twice that) with room for its own retries. So an
26
- * outage that ends inside the window is always seen to end. The Python
27
- * SDK's Outage.
22
+ * (pollOutageMs), from when the first poll's retries ran out, and, while the
23
+ * command runs, never past the wait's own deadline (`waitDeadline`, epoch
24
+ * ms; null for none): polled again with backoff, each poll and its retries
25
+ * ending before the last look, which goes out LAST_LOOK_MAX_MS before the
26
+ * window's end (half the window, for one shorter than twice that) with room
27
+ * for its own retries. So an outage that ends inside the window is always
28
+ * seen to end. The Python SDK's Outage.
28
29
  */
29
30
  export declare class Outage {
30
31
  private readonly windowMs;
32
+ private readonly waitDeadline;
31
33
  private end;
32
34
  private last;
33
35
  private tries;
34
- private readonly room;
35
- constructor(windowMs: number);
36
+ private room;
37
+ private finished;
38
+ constructor(windowMs: number, waitDeadline?: number | null);
39
+ /** The command has ended: draining its output, and an outage meanwhile, are not held to the deadline. */
40
+ finish(): void;
41
+ /**
42
+ * Whether a wait's failed poll (`error`, at `now`; `cut`: its bound cut it
43
+ * short) means the wait's own time ran out: its deadline cut the poll
44
+ * short while the command ran, with no outage being ridden out, so the
45
+ * command was still running when last seen. With no window, any timeout
46
+ * while it runs says so, as it always has. Never once the command has
47
+ * ended. The Python SDK's Outage.ran_out.
48
+ */
49
+ ranOut(error: PrizedError, now: number, cut: boolean): boolean;
36
50
  /** The bound on a poll sent `now`: undefined unless polls are failing; else the last look's start, or the window's end for the last look. */
37
51
  deadline(now: number): number | undefined;
38
52
  /** A poll was answered: the outage, if any, is over. */
@@ -90,16 +104,33 @@ export interface ExecOptions {
90
104
  * its polls fail (no answer, a 5xx, a rate limit), in ms, before it gives
91
105
  * up and stops the command. Default: the client's `pollOutageMs`
92
106
  * (120 000). A refused key, or an answer that settles the run (the
93
- * 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).
94
110
  */
95
111
  pollOutageMs?: number;
96
112
  }
97
113
  export interface WaitOptions {
98
- /** Stop waiting after this many ms (the command keeps running). Default: no limit. */
114
+ /**
115
+ * Stop waiting after this many ms (the command keeps running). Default: no
116
+ * limit. Once the command has ended, reading the rest of its output is not
117
+ * held to it.
118
+ */
99
119
  timeoutMs?: number;
100
120
  onStdout?: (chunk: string) => void;
101
121
  onStderr?: (chunk: string) => void;
102
122
  signal?: AbortSignal;
123
+ /**
124
+ * How long the wait keeps polling while its polls fail (no answer, a 5xx,
125
+ * a rate limit), in ms, before it gives up and throws what the polls met.
126
+ * While the command runs, never past `timeoutMs`. Default: the client's
127
+ * `pollOutageMs` (120 000).
128
+ * `0` gives up once one poll's retries run out. A refused key, or an
129
+ * answer that settles the run (the sandbox deleted or not running, the
130
+ * run gone), ends the wait at once. Giving up stops nothing: the command
131
+ * keeps running, and wait() can be called again.
132
+ */
133
+ pollOutageMs?: number;
103
134
  }
104
135
  export interface CommandStatus {
105
136
  running: boolean;
@@ -136,15 +167,17 @@ export declare class CommandHandle {
136
167
  * `onStdout`/`onStderr` receive the output as it arrives, from the start.
137
168
  * A `timeoutMs` here bounds the wait only; the command keeps running. If
138
169
  * the command ran past its own timeout (the one given to exec), it was
139
- * killed and this throws CommandTimeoutError with what it printed.
170
+ * killed and this throws CommandTimeoutError with what it printed. While
171
+ * polls fail (a dropped connection, a 5xx), it keeps polling for up to
172
+ * `pollOutageMs` (the client's, 120 000 by default) before it throws.
140
173
  */
141
174
  wait(options?: WaitOptions): Promise<ExecResult>;
142
175
  /**
143
176
  * @internal wait() without the timeout check: exec cleans up before it
144
- * throws. `outageMs` (exec's pollOutageMs; a wait with no limit only):
145
- * how long polls may fail before the wait gives up (Outage).
177
+ * throws. `outageMs` (pollOutageMs): how long polls may fail before the
178
+ * wait gives up (Outage), never past the wait's own deadline.
146
179
  */
147
- waitRaw(options?: WaitOptions, outageMs?: number): Promise<ExecResult>;
180
+ waitRaw(options: WaitOptions, outageMs: number): Promise<ExecResult>;
148
181
  private waitTimeout;
149
182
  private result;
150
183
  /**
@@ -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. */
@@ -65,22 +65,42 @@ export function ridesOut(error) {
65
65
  }
66
66
  /**
67
67
  * The failed polls of a wait that rides out an outage of up to `windowMs`
68
- * (exec's pollOutageMs), from when the first poll's retries ran out: polled
69
- * again with backoff, each poll and its retries ending before the last look,
70
- * which goes out LAST_LOOK_MAX_MS before the window's end (half the window,
71
- * for one shorter than twice that) with room for its own retries. So an
72
- * outage that ends inside the window is always seen to end. The Python
73
- * SDK's Outage.
68
+ * (pollOutageMs), from when the first poll's retries ran out, and, while the
69
+ * command runs, never past the wait's own deadline (`waitDeadline`, epoch
70
+ * ms; null for none): polled again with backoff, each poll and its retries
71
+ * ending before the last look, which goes out LAST_LOOK_MAX_MS before the
72
+ * window's end (half the window, for one shorter than twice that) with room
73
+ * for its own retries. So an outage that ends inside the window is always
74
+ * seen to end. The Python SDK's Outage.
74
75
  */
75
76
  export class Outage {
76
77
  windowMs;
78
+ waitDeadline;
77
79
  end = null;
78
80
  last = null;
79
81
  tries = 0;
80
- room;
81
- constructor(windowMs) {
82
+ room = 0;
83
+ finished = false;
84
+ constructor(windowMs, waitDeadline = null) {
82
85
  this.windowMs = windowMs;
83
- this.room = Math.min(LAST_LOOK_MAX_MS, windowMs / 2);
86
+ this.waitDeadline = waitDeadline;
87
+ }
88
+ /** The command has ended: draining its output, and an outage meanwhile, are not held to the deadline. */
89
+ finish() {
90
+ this.finished = true;
91
+ }
92
+ /**
93
+ * Whether a wait's failed poll (`error`, at `now`; `cut`: its bound cut it
94
+ * short) means the wait's own time ran out: its deadline cut the poll
95
+ * short while the command ran, with no outage being ridden out, so the
96
+ * command was still running when last seen. With no window, any timeout
97
+ * while it runs says so, as it always has. Never once the command has
98
+ * ended. The Python SDK's Outage.ran_out.
99
+ */
100
+ ranOut(error, now, cut) {
101
+ if (this.waitDeadline === null || this.finished || error.code !== 'timeout')
102
+ return false;
103
+ return this.windowMs <= 0 || (this.end === null && (cut || now >= this.waitDeadline));
84
104
  }
85
105
  /** The bound on a poll sent `now`: undefined unless polls are failing; else the last look's start, or the window's end for the last look. */
86
106
  deadline(now) {
@@ -104,7 +124,10 @@ export class Outage {
104
124
  failed(error, sentAt, now, cut, backoff) {
105
125
  if (this.windowMs <= 0 || !ridesOut(error))
106
126
  return { pause: null, failure: error };
107
- this.end ??= now + this.windowMs;
127
+ if (this.end === null) {
128
+ this.end = Math.min(now + this.windowMs, this.finished || this.waitDeadline === null ? Infinity : this.waitDeadline);
129
+ this.room = Math.max(0, Math.min(LAST_LOOK_MAX_MS, (this.end - now) / 2));
130
+ }
108
131
  const met = error.cause instanceof PrizedError ? error.cause : null;
109
132
  if (!cut) {
110
133
  this.last = error;
@@ -272,17 +295,20 @@ export class CommandHandle {
272
295
  * `onStdout`/`onStderr` receive the output as it arrives, from the start.
273
296
  * A `timeoutMs` here bounds the wait only; the command keeps running. If
274
297
  * the command ran past its own timeout (the one given to exec), it was
275
- * killed and this throws CommandTimeoutError with what it printed.
298
+ * killed and this throws CommandTimeoutError with what it printed. While
299
+ * polls fail (a dropped connection, a 5xx), it keeps polling for up to
300
+ * `pollOutageMs` (the client's, 120 000 by default) before it throws.
276
301
  */
277
302
  async wait(options = {}) {
278
- return finished(await this.waitRaw(options), this.timeoutMs, this.sandboxId);
303
+ const outageMs = validOutage(options.pollOutageMs) ?? this.toolbox.config.pollOutageMs;
304
+ return finished(await this.waitRaw(options, outageMs), this.timeoutMs, this.sandboxId);
279
305
  }
280
306
  /**
281
307
  * @internal wait() without the timeout check: exec cleans up before it
282
- * throws. `outageMs` (exec's pollOutageMs; a wait with no limit only):
283
- * how long polls may fail before the wait gives up (Outage).
308
+ * throws. `outageMs` (pollOutageMs): how long polls may fail before the
309
+ * wait gives up (Outage), never past the wait's own deadline.
284
310
  */
285
- async waitRaw(options = {}, outageMs = 0) {
311
+ async waitRaw(options, outageMs) {
286
312
  const config = this.toolbox.config;
287
313
  const deadline = options.timeoutMs ? config.now() + options.timeoutMs : null;
288
314
  const out = new StreamReader(options.onStdout);
@@ -290,7 +316,7 @@ export class CommandHandle {
290
316
  let delay = POLL_MIN_MS;
291
317
  let finished = false;
292
318
  const polls = deadline === null ? null : new Polls(config, deadline);
293
- const outage = new Outage(deadline === null ? outageMs : 0);
319
+ const outage = new Outage(outageMs, deadline);
294
320
  for (;;) {
295
321
  let report;
296
322
  const sentAt = config.now();
@@ -305,13 +331,15 @@ export class CommandHandle {
305
331
  polls?.answered();
306
332
  }
307
333
  catch (error) {
308
- if (deadline !== null && error instanceof PrizedError && error.code === 'timeout')
334
+ const now = config.now();
335
+ if (error instanceof PrizedError && outage.ranOut(error, now, cutByDeadline(error))) {
309
336
  throw this.waitTimeout(options.timeoutMs);
337
+ }
310
338
  let failure = error;
311
339
  if (error instanceof PrizedError && !options.signal?.aborted) {
312
340
  // A dropped connection, an edge restarting: the command may be
313
341
  // fine. Poll again until the window runs out, then give up.
314
- const step = outage.failed(error, sentAt, config.now(), cutByDeadline(error), (tries) => backoffMs(config, tries));
342
+ const step = outage.failed(error, sentAt, now, cutByDeadline(error), (tries) => backoffMs(config, tries));
315
343
  if (step.pause !== null) {
316
344
  await config.sleep(step.pause, options.signal);
317
345
  continue;
@@ -323,8 +351,10 @@ export class CommandHandle {
323
351
  throw failure;
324
352
  }
325
353
  outage.answered();
326
- if (report.state !== 'running')
354
+ if (report.state !== 'running') {
327
355
  finished = true;
356
+ outage.finish();
357
+ }
328
358
  out.push(report.stdout);
329
359
  err.push(report.stderr);
330
360
  const more = (report.stdout.length > 0 && out.offset < report.outSize) ||
@@ -440,7 +470,48 @@ async function cleanupOnceGone(toolbox, id) {
440
470
  }
441
471
  }
442
472
  }
443
- 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) {
444
515
  // Nothing sent yet: nothing to stop.
445
516
  if (signal?.aborted)
446
517
  throw withOutcome(new PrizedError('aborted', null, { sandboxId: toolbox.sandboxId }), startEffect(), 'never');
@@ -452,10 +523,7 @@ async function startBackground(toolbox, command, cwd, env, timeoutMs, signal, un
452
523
  // Launched, but no pid came within the start's own wait: it may yet run.
453
524
  let unsettled = false;
454
525
  try {
455
- const result = await rawExec(toolbox, { command: startScript(id, command, timeoutMs === null ? null : timeoutMs / 1000, unbuffered), cwd, env, timeoutMs: 60_000 },
456
- // Keyed by a run id this call chose, so a resend finds the first start
457
- // instead of starting the command twice.
458
- { 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);
459
527
  pid = Number(result.stdout.trim());
460
528
  if (result.exitCode !== 0 || !Number.isInteger(pid) || pid <= 0) {
461
529
  unsettled = result.exitCode === NO_PID_EXIT;
@@ -501,7 +569,7 @@ export async function execCommand(toolbox, command, options = {}) {
501
569
  const cwd = options.cwd === undefined ? undefined : sandboxPath(options.cwd);
502
570
  if (options.background) {
503
571
  const limit = options.timeoutMs ? options.timeoutMs : null;
504
- 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);
505
573
  handle.timeoutMs = limit;
506
574
  return handle;
507
575
  }
@@ -509,7 +577,7 @@ export async function execCommand(toolbox, command, options = {}) {
509
577
  const streamed = options.onStdout !== undefined || options.onStderr !== undefined;
510
578
  if (streamed || timeoutMs === 0 || timeoutMs > MAX_BLOCKING_EXEC_MS) {
511
579
  const limit = timeoutMs === 0 ? null : timeoutMs;
512
- 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);
513
581
  const waitOptions = {};
514
582
  if (options.onStdout)
515
583
  waitOptions.onStdout = options.onStdout;
@@ -3,6 +3,11 @@ import type { ExecResult } from './types.js';
3
3
  export declare const API_KEYS_URL = "https://prized.dev/dashboard/workspace/api-keys";
4
4
  /** Where a newcomer signs up (free credit, no card) and gets one. */
5
5
  export declare const SIGN_UP_URL = "https://prized.dev/onboarding/sandboxes";
6
+ /** What a coding agent follows to sign its person up and get a key itself:
7
+ * their email address, then the 6-digit code Prized emails them. The SDK's
8
+ * own key errors name it; a refused key's 401 carries the API's own line
9
+ * (`signUp`), which follows the deployment. */
10
+ export declare const AGENT_SIGN_UP_URL = "https://prized.dev/auth.md";
6
11
  /** The free tier's limits, and what lifts them. */
7
12
  export declare const FREE_TIER_URL = "https://prized.dev/docs/sandboxes#free-tier";
8
13
  /** The sentence an error with this code and no message of its own carries. */
@@ -17,6 +22,8 @@ export interface PrizedErrorInit {
17
22
  body?: Record<string, unknown> | null;
18
23
  /** Seconds the API asked the caller to wait (Retry-After). */
19
24
  retryAfter?: number | null;
25
+ /** The command's exit code, for an error that came after it ran. */
26
+ exitCode?: number | null;
20
27
  cause?: unknown;
21
28
  }
22
29
  /**
@@ -27,13 +34,21 @@ export interface PrizedErrorInit {
27
34
  * - `code`: the API's error string, e.g. `paid_balance_required`,
28
35
  * `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
29
36
  * `rate_limited`; or one of the SDK's own: `missing_api_key`,
30
- * `invalid_api_key`, `invalid_argument`, `network_error`, `timeout`, `aborted`,
37
+ * `invalid_api_key`, `invalid_argument`, `edge_unknown` (another
38
+ * deployment's API named no command endpoint this SDK sends the key to),
39
+ * `network_error`, `timeout`, `aborted`,
31
40
  * `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
32
- * `command_failed`, `command_timeout` (thrown as CommandTimeoutError).
41
+ * `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
42
+ * `output_read_failed` (thrown as OutputReadError).
33
43
  * - `reason`: the finer-grained reason, when there is one (for
34
44
  * `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
35
45
  * `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
36
- * `max_runtime`).
46
+ * `max_runtime`; for `output_read_failed`, the code of the failure that
47
+ * stopped the read).
48
+ * - `exitCode`: the command's exit code when the error came after it ran: a
49
+ * `fullOutput` command whose output could not be read back
50
+ * (`output_read_failed`, or `sandbox_deleted` when the sandbox was deleted
51
+ * first). Don't run such a command again blindly. Null otherwise.
37
52
  */
38
53
  export declare class PrizedError extends Error {
39
54
  readonly status: number | null;
@@ -42,6 +57,7 @@ export declare class PrizedError extends Error {
42
57
  readonly sandboxId: string | null;
43
58
  readonly body: Record<string, unknown> | null;
44
59
  readonly retryAfter: number | null;
60
+ readonly exitCode: number | null;
45
61
  constructor(code: string, message?: string | null, init?: PrizedErrorInit);
46
62
  toString(): string;
47
63
  }
@@ -61,6 +77,30 @@ export declare class CommandTimeoutError extends PrizedError {
61
77
  /** What the command printed to stderr before it was killed. */
62
78
  get stderr(): string;
63
79
  }
80
+ export interface OutputReadErrorInit {
81
+ /** The failure that stopped the read: a download's error. */
82
+ cause: PrizedError;
83
+ exitCode: number;
84
+ stdoutPath: string;
85
+ stderrPath: string;
86
+ }
87
+ /**
88
+ * A `fullOutput` command ran, but its output could not be read back from the
89
+ * files it was copied to (`code` is `output_read_failed`). The command did
90
+ * run (`exitCode` is its exit code), so do not run it again blindly; and the
91
+ * sandbox is not known to be gone (a deleted one throws `sandbox_deleted`
92
+ * instead). `reason` is the code of the failure that stopped the read (say
93
+ * `not_found`, `network_error`), and `cause` is that error, with its HTTP
94
+ * status; `status` is null, as the command's own call succeeded. The output
95
+ * stays in the sandbox for an hour at `stdoutPath` and `stderrPath`: read it
96
+ * with files.read(), then remove their directory.
97
+ */
98
+ export declare class OutputReadError extends PrizedError {
99
+ readonly exitCode: number;
100
+ readonly stdoutPath: string;
101
+ readonly stderrPath: string;
102
+ constructor(message: string, init: OutputReadErrorInit);
103
+ }
64
104
  /** Retry-After as seconds: delta-seconds or an HTTP date. */
65
105
  export declare function parseRetryAfter(value: string | null, nowMs: number): number | null;
66
106
  /**
@@ -2,15 +2,22 @@
2
2
  export const API_KEYS_URL = 'https://prized.dev/dashboard/workspace/api-keys';
3
3
  /** Where a newcomer signs up (free credit, no card) and gets one. */
4
4
  export const SIGN_UP_URL = 'https://prized.dev/onboarding/sandboxes';
5
+ /** What a coding agent follows to sign its person up and get a key itself:
6
+ * their email address, then the 6-digit code Prized emails them. The SDK's
7
+ * own key errors name it; a refused key's 401 carries the API's own line
8
+ * (`signUp`), which follows the deployment. */
9
+ export const AGENT_SIGN_UP_URL = 'https://prized.dev/auth.md';
10
+ 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 ${AGENT_SIGN_UP_URL}`;
5
11
  /** The free tier's limits, and what lifts them. */
6
12
  export const FREE_TIER_URL = 'https://prized.dev/docs/sandboxes#free-tier';
7
13
  /** A sentence for the codes a caller is most likely to meet, used when the
8
14
  * answer carries no `message` of its own. */
9
15
  const MESSAGES = {
10
16
  // The SDK's own codes.
11
- missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${SIGN_UP_URL} signs you up and makes one; otherwise make one at ${API_KEYS_URL}.`,
12
- invalid_api_key: `The API key has characters no key has (the docs' placeholder, or a stray character?); copy it again from ${API_KEYS_URL}.`,
17
+ missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${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 ${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 ${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 ${API_KEYS_URL}. ${AGENT_SIGN_UP}`,
13
19
  invalid_argument: 'An argument was not valid.',
20
+ edge_unknown: 'The API named no command endpoint this SDK trusts; pass edgeUrl for that deployment.',
14
21
  network_error: 'Could not reach the Prized API.',
15
22
  timeout: 'Timed out.',
16
23
  aborted: 'The operation was aborted.',
@@ -21,6 +28,7 @@ const MESSAGES = {
21
28
  sandbox_deleted: 'The sandbox was deleted.',
22
29
  command_failed: 'The command failed in the sandbox.',
23
30
  command_timeout: 'The command ran past its timeout and was killed.',
31
+ output_read_failed: 'The command ran, but its output could not be read back from the sandbox.',
24
32
  not_directory: 'Not a directory.',
25
33
  // The API's and the exec edge's codes.
26
34
  unauthorized: `The API key was refused. Check that it is a live key from ${API_KEYS_URL}.`,
@@ -77,13 +85,21 @@ export function defaultMessage(code) {
77
85
  * - `code`: the API's error string, e.g. `paid_balance_required`,
78
86
  * `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
79
87
  * `rate_limited`; or one of the SDK's own: `missing_api_key`,
80
- * `invalid_api_key`, `invalid_argument`, `network_error`, `timeout`, `aborted`,
88
+ * `invalid_api_key`, `invalid_argument`, `edge_unknown` (another
89
+ * deployment's API named no command endpoint this SDK sends the key to),
90
+ * `network_error`, `timeout`, `aborted`,
81
91
  * `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
82
- * `command_failed`, `command_timeout` (thrown as CommandTimeoutError).
92
+ * `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
93
+ * `output_read_failed` (thrown as OutputReadError).
83
94
  * - `reason`: the finer-grained reason, when there is one (for
84
95
  * `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
85
96
  * `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
86
- * `max_runtime`).
97
+ * `max_runtime`; for `output_read_failed`, the code of the failure that
98
+ * stopped the read).
99
+ * - `exitCode`: the command's exit code when the error came after it ran: a
100
+ * `fullOutput` command whose output could not be read back
101
+ * (`output_read_failed`, or `sandbox_deleted` when the sandbox was deleted
102
+ * first). Don't run such a command again blindly. Null otherwise.
87
103
  */
88
104
  export class PrizedError extends Error {
89
105
  status;
@@ -92,6 +108,7 @@ export class PrizedError extends Error {
92
108
  sandboxId;
93
109
  body;
94
110
  retryAfter;
111
+ exitCode;
95
112
  constructor(code, message, init = {}) {
96
113
  super(message || defaultMessage(code), init.cause === undefined ? undefined : { cause: init.cause });
97
114
  this.name = 'PrizedError';
@@ -101,6 +118,7 @@ export class PrizedError extends Error {
101
118
  this.sandboxId = init.sandboxId ?? null;
102
119
  this.body = init.body ?? null;
103
120
  this.retryAfter = init.retryAfter ?? null;
121
+ this.exitCode = init.exitCode ?? null;
104
122
  }
105
123
  toString() {
106
124
  const head = this.status === null ? this.code : `${this.status} ${this.code}`;
@@ -135,6 +153,30 @@ export class CommandTimeoutError extends PrizedError {
135
153
  return this.result.stderr;
136
154
  }
137
155
  }
156
+ /**
157
+ * A `fullOutput` command ran, but its output could not be read back from the
158
+ * files it was copied to (`code` is `output_read_failed`). The command did
159
+ * run (`exitCode` is its exit code), so do not run it again blindly; and the
160
+ * sandbox is not known to be gone (a deleted one throws `sandbox_deleted`
161
+ * instead). `reason` is the code of the failure that stopped the read (say
162
+ * `not_found`, `network_error`), and `cause` is that error, with its HTTP
163
+ * status; `status` is null, as the command's own call succeeded. The output
164
+ * stays in the sandbox for an hour at `stdoutPath` and `stderrPath`: read it
165
+ * with files.read(), then remove their directory.
166
+ */
167
+ export class OutputReadError extends PrizedError {
168
+ exitCode;
169
+ stdoutPath;
170
+ stderrPath;
171
+ constructor(message, init) {
172
+ const { cause } = init;
173
+ super('output_read_failed', message, { reason: cause.code, sandboxId: cause.sandboxId, cause, exitCode: init.exitCode });
174
+ this.name = 'OutputReadError';
175
+ this.exitCode = init.exitCode;
176
+ this.stdoutPath = init.stdoutPath;
177
+ this.stderrPath = init.stderrPath;
178
+ }
179
+ }
138
180
  function stringField(record, key) {
139
181
  const value = record[key];
140
182
  return typeof value === 'string' && value !== '' ? value : null;
@@ -246,5 +288,10 @@ export function errorFromResponse(status, text, retryAfter, sandboxId, path = ''
246
288
  const reason = doc ? stringField(doc, 'reason') ?? stringField(doc, 'field') ?? stringField(doc, 'detail') : null;
247
289
  if (doc && doc.tier === 'free' && !(message ?? '').includes('free tier'))
248
290
  message = freeTierMessage(code, message, reason, doc.limit);
291
+ // A refused key's 401 may say how an agent signs its person up (the API
292
+ // sends it where agent sign-in answers, with that deployment's address).
293
+ const signUp = code === 'unauthorized' && doc ? stringField(doc, 'signUp') : null;
294
+ if (signUp !== null)
295
+ message = `${message ?? defaultMessage(code)} ${signUp.slice(0, 300)}`;
249
296
  return new PrizedError(code, message, { status, reason, sandboxId, body: doc, retryAfter });
250
297
  }
@@ -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