@shardflux/sdk 0.11.1 → 0.13.0

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.
@@ -80,6 +80,45 @@ export interface paths {
80
80
  * `503 service_unavailable` (`no_execution_host`: no host with
81
81
  * protocol feature `file_first` has room; retry), secret resolution
82
82
  * errors as for processful starts.
83
+ *
84
+ * Burst execution (contracts §33, not public yet): `burst: always` runs
85
+ * the command on a larger, short-lived burst VM on the workspace's
86
+ * host over a copy of the workspace disk and applies its file changes
87
+ * back when it exits. The session is read, streamed and attached like
88
+ * any other; it carries `burst` (BurstSummary) and cannot be signalled
89
+ * or canceled (`409 conflict`, `details.reason burst_not_supported`).
90
+ * Refusals: `422 validation_failed` with `details.reason`
91
+ * `burst_mode_not_supported` (`auto`), `burst_not_supported` (stdin),
92
+ * `burst_size_exceeds_plan` (`burst_vcpus`/`burst_memory_mib` above the
93
+ * plan's per-workspace ceilings; `details.field`, `details.limit`);
94
+ * `409 burst_unavailable` with `details.reason` `not_available` (no
95
+ * entitlement `policy.burst_exec`, or the host lacks protocol feature
96
+ * `burst_exec`), `layout_unsupported`, `shared_volumes`,
97
+ * `host_capacity`, `fence_not_drained`, `workspace_fenced`,
98
+ * `apply_pending`, `park_failed`, `workspace_resumed`, `interrupted`;
99
+ * `409 burst_apply_failed` (`details.reason` `disk_full` or
100
+ * `apply_failed`, `details.applied_entries`, `details.pending_entries`;
101
+ * the workspace refuses writes until a retry with the same
102
+ * `session_id` completes the apply). A refusal the host gives within
103
+ * the first seconds is the start's answer; a later one ends the
104
+ * session (`failed_to_start` for `burst_unavailable`, `lost` for
105
+ * `burst_apply_failed`), is recorded in `burst.error`, and is the
106
+ * output stream's final `error` event. A retry with the same
107
+ * `session_id` replays the recorded outcome (the host journals the
108
+ * burst by the exec's operation id) or resumes an unfinished apply.
109
+ *
110
+ * Start and follow in one request: a processful start sent with
111
+ * `Accept: application/x-ndjson` (or `text/event-stream`) answers 200
112
+ * with the session's output stream, as `GET .../output?follow=true`
113
+ * from offset 0 (output events, `exit`, a final `error`), and
114
+ * `X-Exec-Session-Id` names the session. The stream slot is taken
115
+ * before the command starts (`503 service_unavailable` when the
116
+ * gateway has no stream slot: nothing ran). Headers are sent as soon
117
+ * as the command started, before any output. A `failed_to_start`
118
+ * session, a refusal and a file-first execution keep their JSON
119
+ * answers. A replayed `session_id` streams the existing session and
120
+ * never runs it again; after a dropped stream, reconnect with
121
+ * `GET .../output` from the byte offsets already read.
83
122
  */
84
123
  post: operations["execStart"];
85
124
  delete?: never;
@@ -170,6 +209,29 @@ export interface paths {
170
209
  patch?: never;
171
210
  trace?: never;
172
211
  };
212
+ "/v1/workspaces/{workspace_id}/exec/{session_id}/stdin": {
213
+ parameters: {
214
+ query?: never;
215
+ header?: never;
216
+ path: {
217
+ workspace_id: components["parameters"]["WorkspaceId"];
218
+ session_id: components["parameters"]["SessionId"];
219
+ };
220
+ cookie?: never;
221
+ };
222
+ get?: never;
223
+ put?: never;
224
+ /**
225
+ * Write pipe stdin or send EOF to an exec session
226
+ * @description Requires stdin_open at start (processful only), host exec_stdin and guest exec_stdin.v1. Offset is the total bytes previously accepted (initially 0). A repeated identical last frame is safe; a different stale offset is refused. Writes are bounded: offset in the response may acknowledge only a prefix; continue from that offset. close takes effect only after the whole frame is accepted. The transport writes no input log or payload file; program output and full-state snapshots retain their normal persistence. An ended session refuses input.
227
+ */
228
+ post: operations["execInput"];
229
+ delete?: never;
230
+ options?: never;
231
+ head?: never;
232
+ patch?: never;
233
+ trace?: never;
234
+ };
173
235
  "/v1/workspaces/{workspace_id}/exec/{session_id}/signal": {
174
236
  parameters: {
175
237
  query?: never;
@@ -202,7 +264,16 @@ export interface paths {
202
264
  };
203
265
  get?: never;
204
266
  put?: never;
205
- /** SIGTERM the process group, SIGKILL after grace */
267
+ /**
268
+ * SIGTERM the process group, SIGKILL after grace
269
+ * @description Sends SIGTERM to the session's process group and SIGKILL after
270
+ * `grace_ms` (0 to 60000; 0 or omitted is 5000). Answers when the
271
+ * command has ended: at once when SIGTERM ends it, otherwise after the
272
+ * grace and the SIGKILL. A `grace_ms` outside the range is `422
273
+ * validation_failed`
274
+ * (`details.field` `grace_ms`, `details.minimum`, `details.maximum`).
275
+ * An ended session is returned as it is.
276
+ */
206
277
  post: operations["execCancel"];
207
278
  delete?: never;
208
279
  options?: never;
@@ -270,7 +341,11 @@ export interface paths {
270
341
  get: operations["ptyGet"];
271
342
  put?: never;
272
343
  post?: never;
273
- /** Close (SIGHUP, then SIGKILL after grace) */
344
+ /**
345
+ * Close (SIGHUP, then SIGKILL after grace)
346
+ * @description Sends SIGHUP to the session's process group and SIGKILL 2 s later
347
+ * if it is still running; answers once it has ended.
348
+ */
274
349
  delete: operations["ptyClose"];
275
350
  options?: never;
276
351
  head?: never;
@@ -721,8 +796,9 @@ export interface paths {
721
796
  * @description `idle_since` = max(`last_tool_at`, `last_attach_at`, `keepalive_until`,
722
797
  * `running_since`, the work horizon of running exec commands) (null when
723
798
  * none is known; in the future while a keepalive or a running command is
724
- * active). A running exec command counts as work until min(its start +
725
- * `IDLE_COMMAND_MAX_SECONDS`, its start + `timeout_ms`), and its end is
799
+ * active). A running exec command counts as work until it ends or its
800
+ * timeout passes: its start + `timeout_ms` when it has one, else its
801
+ * start + `IDLE_COMMAND_MAX_SECONDS` (1 hour by default). Its end is
726
802
  * activity (contracts §20.6): an end seen without a client call is
727
803
  * recorded in `last_attach_at`, which is work but not a tool call.
728
804
  * `suspend_at` = `idle_since` + `idle_minutes` when
@@ -800,7 +876,7 @@ export type webhooks = Record<string, never>;
800
876
  export interface components {
801
877
  schemas: {
802
878
  /** @enum {string} */
803
- ErrorCode: "bad_request" | "unauthenticated" | "forbidden" | "not_found" | "conflict" | "stale_epoch" | "workspace_not_running" | "validation_failed" | "payload_too_large" | "range_not_satisfiable" | "rate_limited" | "capacity_pending" | "dependency_unavailable" | "timeout" | "internal_error" | "service_unavailable" | "workspace_busy";
879
+ ErrorCode: "bad_request" | "unauthenticated" | "forbidden" | "not_found" | "conflict" | "stale_epoch" | "workspace_not_running" | "validation_failed" | "payload_too_large" | "range_not_satisfiable" | "rate_limited" | "capacity_pending" | "dependency_unavailable" | "timeout" | "internal_error" | "service_unavailable" | "workspace_busy" | "burst_unavailable" | "burst_apply_failed";
804
880
  ErrorBody: {
805
881
  error: {
806
882
  code: components["schemas"]["ErrorCode"];
@@ -843,11 +919,16 @@ export interface components {
843
919
  /** @description Absolute working directory; omitted or empty starts in the default (/home/user). A relative path is refused, not resolved: 422 validation_failed, details.reason invalid_cwd, details.field cwd, the message naming the absolute path it likely means. A cwd that is not a directory ends the session failed_to_start (processful) or the execution failed with details.reason exec_failed_to_start (file-first). */
844
920
  cwd?: string;
845
921
  user?: string;
922
+ /** @description Keep a pipe open for exec stdin writes; processful only, mutually exclusive with stdin. Requires exec_stdin.v1. */
923
+ stdin_open?: boolean;
846
924
  /** @description Written to stdin, which is then closed (max 1 MiB decoded). */
847
925
  stdin?: string;
848
926
  /** Format: int64 */
849
927
  timeout_ms?: number;
850
- /** Format: int64 */
928
+ /**
929
+ * Format: int64
930
+ * @description Time between the SIGTERM and the SIGKILL when timeout_ms ends the command; 0 or omitted is 5000.
931
+ */
851
932
  kill_grace_ms?: number;
852
933
  secret_refs?: components["schemas"]["SecretRefs"];
853
934
  /** @description File-first workspaces (required there, refused otherwise) — the execution's idempotency key. */
@@ -858,6 +939,16 @@ export interface components {
858
939
  * @default 1048576
859
940
  */
860
941
  output_limit_bytes?: number;
942
+ /**
943
+ * @description Burst execution (contracts §33): `always` runs this run-to-completion command on a larger, short-lived burst VM and applies its file changes back. Needs entitlement `policy.burst_exec` and a layered processful workspace; not with stdin. `auto` is reserved (422 validation_failed, details.reason burst_mode_not_supported).
944
+ * @default never
945
+ * @enum {string}
946
+ */
947
+ burst?: "never" | "always";
948
+ /** @description The burst VM's vCPUs (burst always only; default the host's, 8), at most the plan's workspace CPU ceiling. */
949
+ burst_vcpus?: number;
950
+ /** @description The burst VM's memory in MiB (burst always only; default the host's, 8192), at most the plan's workspace memory ceiling. */
951
+ burst_memory_mib?: number;
861
952
  };
862
953
  /** @description Names of customer secrets (contracts §17) to inject as environment variables NAME=value of this process only. The cell resolves them at session start through the API with the caller's tool token (permission-checked and audited by the API); values are never logged, persisted or returned by the cell. Any name the caller may not use (unknown, not permitted for this workspace/project/tool) refuses the whole start with 403 forbidden (details.reason secret_not_available, details.names); nothing is started. A name that is also a key of `env` is refused (422 validation_failed). Requires a workspace tool token (browser stream tickets cannot resolve secrets: 403, details.reason secret_refs_require_tool_token). Resolution failures: 401 (token revoked/expired), 409 stale_epoch, 429 rate_limited, 503 dependency_unavailable. A retried start with the same session_id re-resolves the names; if a value changed since the first start the host refuses the different request (409 conflict). */
863
954
  SecretRefs: string[];
@@ -955,6 +1046,88 @@ export interface components {
955
1046
  ended_at?: string | null;
956
1047
  /** @description Why the session ended without an exit code. For failed_to_start the command never ran and this names the cause, e.g. `working directory "/home/user/app" is not a directory` or `executable "foo" not found in PATH`. */
957
1048
  error?: string;
1049
+ memory_grow?: components["schemas"]["MemoryGrow"];
1050
+ burst?: components["schemas"]["BurstSummary"];
1051
+ };
1052
+ /** @description Burst execution (contracts §33.5): present on every session started with `burst: always`. The size, method and counts are set once the burst ended (the host's result); while it runs only host, applied and the requested size are known. */
1053
+ BurstSummary: {
1054
+ /**
1055
+ * @description Where the burst VM ran; `local` = the workspace's host (`remote` is reserved).
1056
+ * @enum {string}
1057
+ */
1058
+ host: "local" | "remote";
1059
+ /** @description The burst VM's vCPUs (the requested count while it runs, absent for the host default). */
1060
+ vcpus?: number;
1061
+ /** @description The burst VM's memory in MiB (the requested size while it runs, absent for the host default). */
1062
+ memory_mib?: number;
1063
+ /**
1064
+ * @description How the burst VM got the workspace copy.
1065
+ * @enum {string}
1066
+ */
1067
+ method?: "layer" | "clone";
1068
+ /** @description The command's file changes were applied to the workspace. */
1069
+ applied: boolean;
1070
+ /** Format: int64 */
1071
+ written_files?: number;
1072
+ /** Format: int64 */
1073
+ written_dirs?: number;
1074
+ /** Format: int64 */
1075
+ removed?: number;
1076
+ /** Format: int64 */
1077
+ written_bytes?: number;
1078
+ /** @description Processes the command left behind in the burst VM (killed; they do not survive a burst). */
1079
+ leftover_killed?: number;
1080
+ /**
1081
+ * Format: int64
1082
+ * @description Wall time of the burst minus the command's own run time.
1083
+ */
1084
+ overhead_ms?: number;
1085
+ /** @description The burst VM's own paths whose changes are never carried back (the host's denylist). */
1086
+ excluded_paths?: string[];
1087
+ /** @description The host's phase timings (ms) and counters. */
1088
+ timings?: {
1089
+ [key: string]: number;
1090
+ };
1091
+ /** @description The recorded outcome of an earlier attempt with the same session_id. */
1092
+ replayed?: boolean;
1093
+ /** @description Set when the burst failed after the start answered: burst_unavailable (the workspace is unchanged) or burst_apply_failed (details.applied_entries, details.pending_entries). */
1094
+ error?: components["schemas"]["BurstError"];
1095
+ };
1096
+ BurstError: {
1097
+ code: components["schemas"]["ErrorCode"];
1098
+ message: string;
1099
+ retryable: boolean;
1100
+ details?: {
1101
+ [key: string]: unknown;
1102
+ };
1103
+ };
1104
+ /** @description Elastic memory (contracts §32.3, §32.6): the host grew the workspace's memory before this exec started (the exec waited for it). Present only on the session returned by the start that grew the VM; absent when no grow ran (fixed workspaces, or already at the exec size). */
1105
+ MemoryGrow: {
1106
+ /**
1107
+ * @description delivered: the whole grow; partial: less than wanted; missed: nothing could be plugged; failed: the plug did not converge or errored.
1108
+ * @enum {string}
1109
+ */
1110
+ outcome: "delivered" | "partial" | "missed" | "failed";
1111
+ /**
1112
+ * Format: int64
1113
+ * @description Guest memory before the grow (MiB).
1114
+ */
1115
+ from_mib: number;
1116
+ /**
1117
+ * Format: int64
1118
+ * @description Guest memory the grow aimed for (MiB).
1119
+ */
1120
+ want_mib: number;
1121
+ /**
1122
+ * Format: int64
1123
+ * @description Guest memory after the grow (MiB).
1124
+ */
1125
+ got_mib: number;
1126
+ /**
1127
+ * Format: int64
1128
+ * @description Trigger to plugged and guest MemTotal settled (ms, rounded).
1129
+ */
1130
+ deliver_ms: number;
958
1131
  };
959
1132
  OutputEvent: {
960
1133
  /** @enum {string} */
@@ -974,11 +1147,27 @@ export interface components {
974
1147
  /** @enum {string} */
975
1148
  type: "signal" | "cancel";
976
1149
  signal?: components["schemas"]["SignalValue"];
977
- /** Format: int64 */
1150
+ /**
1151
+ * Format: int64
1152
+ * @description For cancel, as CancelRequest.grace_ms; out of range is an `error` event (validation_failed).
1153
+ */
978
1154
  grace_ms?: number;
979
1155
  };
980
1156
  /** @description Signal number (1-64) or name (e.g. "SIGTERM", "TERM"). */
981
1157
  SignalValue: number | string;
1158
+ ExecInputRequest: {
1159
+ /** @description At most 64 KiB decoded. */
1160
+ data?: string;
1161
+ /** Format: int64 */
1162
+ offset: number;
1163
+ /** @default false */
1164
+ close?: boolean;
1165
+ };
1166
+ ExecInputResult: {
1167
+ /** Format: int64 */
1168
+ offset: number;
1169
+ closed: boolean;
1170
+ };
982
1171
  SignalRequest: {
983
1172
  signal: components["schemas"]["SignalValue"];
984
1173
  /**
@@ -988,7 +1177,10 @@ export interface components {
988
1177
  only_leader?: boolean;
989
1178
  };
990
1179
  CancelRequest: {
991
- /** Format: int64 */
1180
+ /**
1181
+ * Format: int64
1182
+ * @description Time between the SIGTERM and the SIGKILL; 0 or omitted is 5000.
1183
+ */
992
1184
  grace_ms?: number;
993
1185
  };
994
1186
  PtyOpenRequest: {
@@ -1507,12 +1699,16 @@ export interface operations {
1507
1699
  };
1508
1700
  };
1509
1701
  responses: {
1510
- /** @description The session already existed; nothing was run again (processful), or the recorded result of an existing execution (file-first). */
1702
+ /** @description An existing session/result, or followed output when a processful start accepts NDJSON/SSE. A failed-to-start session stays JSON. */
1511
1703
  200: {
1512
1704
  headers: {
1705
+ /** @description The streamed session id; replay/reconnect use the same session and byte offsets. */
1706
+ "X-Exec-Session-Id"?: string;
1513
1707
  [name: string]: unknown;
1514
1708
  };
1515
1709
  content: {
1710
+ "application/x-ndjson": string;
1711
+ "text/event-stream": string;
1516
1712
  "application/json": components["schemas"]["ExecSession"] | components["schemas"]["ExecutionResult"];
1517
1713
  };
1518
1714
  };
@@ -1607,6 +1803,34 @@ export interface operations {
1607
1803
  default: components["responses"]["Error"];
1608
1804
  };
1609
1805
  };
1806
+ execInput: {
1807
+ parameters: {
1808
+ query?: never;
1809
+ header?: never;
1810
+ path: {
1811
+ workspace_id: components["parameters"]["WorkspaceId"];
1812
+ session_id: components["parameters"]["SessionId"];
1813
+ };
1814
+ cookie?: never;
1815
+ };
1816
+ requestBody: {
1817
+ content: {
1818
+ "application/json": components["schemas"]["ExecInputRequest"];
1819
+ };
1820
+ };
1821
+ responses: {
1822
+ /** @description Acknowledged input offset and EOF state. */
1823
+ 200: {
1824
+ headers: {
1825
+ [name: string]: unknown;
1826
+ };
1827
+ content: {
1828
+ "application/json": components["schemas"]["ExecInputResult"];
1829
+ };
1830
+ };
1831
+ default: components["responses"]["Error"];
1832
+ };
1833
+ };
1610
1834
  execSignal: {
1611
1835
  parameters: {
1612
1836
  query?: never;
package/dist/http.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { RetryRecord } from './progress.js';
2
- export declare const SDK_VERSION = "0.11.1";
2
+ export declare const SDK_VERSION = "0.13.0";
3
3
  export interface RequestOptions {
4
4
  query?: Record<string, string | number | boolean | undefined | null>;
5
5
  json?: unknown;
@@ -13,6 +13,11 @@ export interface RequestOptions {
13
13
  signal?: AbortSignal;
14
14
  /** Override the client's default request timeout (ms); 0 disables it (streams). */
15
15
  timeoutMs?: number;
16
+ /**
17
+ * Bounds each attempt until the response headers arrive (ms), leaving the body unbounded: a stream that answers a
18
+ * start (exec.run's combined start and output). Usually with `timeoutMs: 0`.
19
+ */
20
+ headersTimeoutMs?: number;
16
21
  /** Called before each retry of a transient failure (lifecycle timing records these). */
17
22
  onRetry?: (retry: Omit<RetryRecord, 'atMs'>) => void;
18
23
  }
@@ -29,9 +34,9 @@ export interface HttpOptions {
29
34
  }
30
35
  export declare const defaultSleep: (ms: number) => Promise<void>;
31
36
  /**
32
- * The fetch the SDK uses when none is given. On Node 26 every request uses a fresh connection (`Connection: close`),
33
- * as in the CLI and the MCP server; other runtimes reuse connections. `SHARDFLUX_HTTP_KEEPALIVE=1` reuses connections
34
- * on Node 26 too.
37
+ * Reuses TLS connections. Node 26's bundled undici 8.9 can stall reused connections: use pinned undici with a
38
+ * private HTTP/1.1-only dispatcher. Other runtimes retain native fetch. SHARDFLUX_HTTP_KEEPALIVE=0 forces close
39
+ * for diagnosis; =1 retains the historical opt-in to native pooling. An explicitly supplied fetch is untouched.
35
40
  */
36
41
  export declare function defaultFetch(env?: Record<string, string | undefined> | undefined, undiciVersion?: string | undefined): typeof fetch;
37
42
  export declare function buildUrl(base: string, path: string, query?: RequestOptions['query']): string;
package/dist/http.js CHANGED
@@ -8,23 +8,37 @@
8
8
  */
9
9
  import { ShardfluxApiError, ShardfluxProtocolError, apiError, isErrorBody } from "./errors.js";
10
10
  import { describeFailure } from "./progress.js";
11
- export const SDK_VERSION = '0.11.1';
11
+ export const SDK_VERSION = '0.13.0';
12
12
  export const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
13
+ /** One private HTTP/1.1 pool on Node 26+, shared by SDK clients. No global dispatcher changes. */
14
+ let nodeFetch;
13
15
  /**
14
- * The fetch the SDK uses when none is given. On Node 26 every request uses a fresh connection (`Connection: close`),
15
- * as in the CLI and the MCP server; other runtimes reuse connections. `SHARDFLUX_HTTP_KEEPALIVE=1` reuses connections
16
- * on Node 26 too.
16
+ * Reuses TLS connections. Node 26's bundled undici 8.9 can stall reused connections: use pinned undici with a
17
+ * private HTTP/1.1-only dispatcher. Other runtimes retain native fetch. SHARDFLUX_HTTP_KEEPALIVE=0 forces close
18
+ * for diagnosis; =1 retains the historical opt-in to native pooling. An explicitly supplied fetch is untouched.
17
19
  */
18
20
  export function defaultFetch(env = globalThis.process?.env, undiciVersion = globalThis.process?.versions?.undici) {
19
21
  // Why a fresh connection on Node 26 (its bundled HTTP client, measured): docs/progress/startup-latency.md.
20
22
  const base = (input, init) => fetch(input, init);
21
- const major = Number((undiciVersion ?? '').split('.')[0]);
22
- if (!(major >= 8) || env?.SHARDFLUX_HTTP_KEEPALIVE === '1')
23
+ if (env?.SHARDFLUX_HTTP_KEEPALIVE === '0')
24
+ return (input, init) => {
25
+ const headers = new Headers(init?.headers);
26
+ headers.set('connection', 'close');
27
+ return base(input, { ...init, headers });
28
+ };
29
+ if (Number((undiciVersion ?? '').split('.')[0]) < 8 || !undiciVersion || env?.SHARDFLUX_HTTP_KEEPALIVE === '1')
23
30
  return base;
24
- return (input, init) => {
25
- const headers = new Headers(init?.headers);
26
- headers.set('connection', 'close');
27
- return fetch(input, { ...init, headers });
31
+ return async (input, init) => {
32
+ nodeFetch ??= import('undici').then(({ Agent, fetch: pooledFetch }) => {
33
+ const dispatcher = new Agent({ allowH2: false, pipelining: 1 });
34
+ return async (input, init) => {
35
+ const response = await pooledFetch(input, { ...init, dispatcher });
36
+ // Web-standard runtime shape; undici and DOM iterator declarations differ.
37
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Required by consumers compiling with lib.dom instead of only Node types.
38
+ return response;
39
+ };
40
+ });
41
+ return (await nodeFetch)(input, init);
28
42
  };
29
43
  }
30
44
  export function buildUrl(base, path, query) {
@@ -62,7 +76,7 @@ export async function errorFrom(res, source) {
62
76
  }
63
77
  if (isErrorBody(parsed))
64
78
  return apiError(res.status, parsed, source, retryAfterSeconds(res), treeRevisionOf(res.headers) ?? undefined);
65
- return new ShardfluxProtocolError(`HTTP ${res.status} from ${source === 'api' ? 'the Shardflux API' : 'the cell gateway'} without an error body`, res.status);
79
+ return new ShardfluxProtocolError(`HTTP ${res.status} from ${source === 'api' ? 'the Shardflux API' : 'the cell gateway'} without an error body`, res.status, source);
66
80
  }
67
81
  export class HttpClient {
68
82
  opts;
@@ -94,7 +108,13 @@ export class HttpClient {
94
108
  }
95
109
  const timeoutMs = init.timeoutMs ?? this.opts.timeoutMs;
96
110
  const timeout = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
97
- const signal = timeout && init.signal ? AbortSignal.any([timeout, init.signal]) : (timeout ?? init.signal);
111
+ const headersMs = init.headersTimeoutMs ?? 0;
112
+ const headersAbort = headersMs > 0 ? new AbortController() : undefined;
113
+ const headersTimer = headersAbort
114
+ ? setTimeout(() => headersAbort.abort(new DOMException('The response headers timed out.', 'TimeoutError')), headersMs)
115
+ : undefined;
116
+ const signals = [timeout, init.signal, headersAbort?.signal].filter((s) => s !== undefined);
117
+ const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
98
118
  let res;
99
119
  try {
100
120
  res = await this.opts.fetch(url, { method, headers, ...(body === undefined ? {} : { body: body }), ...(signal ? { signal } : {}) });
@@ -105,10 +125,13 @@ export class HttpClient {
105
125
  if (!retriable || attempt >= this.opts.maxRetries)
106
126
  throw err;
107
127
  const delayMs = Math.min(2_000, 200 * 2 ** attempt);
108
- init.onRetry?.({ request: `${method} ${path}`, attempt: attempt + 1, cause: describeFailure(err, timeoutMs), delayMs });
128
+ init.onRetry?.({ request: `${method} ${path}`, attempt: attempt + 1, cause: describeFailure(err, headersAbort?.signal.aborted ? headersMs : timeoutMs), delayMs });
109
129
  await sleep(delayMs);
110
130
  continue;
111
131
  }
132
+ finally {
133
+ clearTimeout(headersTimer);
134
+ }
112
135
  if (res.ok) {
113
136
  if (this.opts.onSuccess) {
114
137
  try {
@@ -142,7 +165,7 @@ export class HttpClient {
142
165
  return JSON.parse(text);
143
166
  }
144
167
  catch {
145
- throw new ShardfluxProtocolError(`${method} ${path}: response is not JSON`, res.status);
168
+ throw new ShardfluxProtocolError(`${method} ${path}: response is not JSON`, res.status, this.opts.source);
146
169
  }
147
170
  }
148
171
  /** Like json() but also returns the status and the response headers (bounded waits read Preference-Applied). */
@@ -153,7 +176,7 @@ export class HttpClient {
153
176
  return { status: res.status, headers: res.headers, body: (text.length === 0 ? undefined : JSON.parse(text)) };
154
177
  }
155
178
  catch {
156
- throw new ShardfluxProtocolError(`${method} ${path}: response is not JSON`, res.status);
179
+ throw new ShardfluxProtocolError(`${method} ${path}: response is not JSON`, res.status, this.opts.source);
157
180
  }
158
181
  }
159
182
  /** Like json() but also returns the HTTP status (open() distinguishes 200 from 202). */
@@ -164,7 +187,7 @@ export class HttpClient {
164
187
  return { status: res.status, body: (text.length === 0 ? undefined : JSON.parse(text)) };
165
188
  }
166
189
  catch {
167
- throw new ShardfluxProtocolError(`${method} ${path}: response is not JSON`, res.status);
190
+ throw new ShardfluxProtocolError(`${method} ${path}: response is not JSON`, res.status, this.opts.source);
168
191
  }
169
192
  }
170
193
  }
package/dist/index.d.ts CHANGED
@@ -15,10 +15,10 @@
15
15
  export type { components, operations, paths } from './generated/app-api.js';
16
16
  export type { components as CellComponents, paths as CellPaths } from './generated/cell-api.js';
17
17
  export { BillingApi, Shardflux, WorkspacesApi, fetchBillingCatalog, pickByKey } from './client.js';
18
- export type { AgentSession, BillingCatalog, BillingSubscription, Caps, CheckoutSession, DiskLayout, Entitlements, FindByKeyOptions, ForkTarget, Invoice, InvoicePage, LifetimeFilter, ListParams, Me, OpenParams, OpenResponse, Operation, Page, PortalSession, PurposeFilter, ResetWorkspaceBody, ResumeAnswer, ResumeRequestOptions, ResumeResponse, ShardfluxOptions, SuspendRequest, SuspendWhenIdleOptions, SuspendWhenIdleResponse, SuspendWhenIdleResult, UpdatePolicy, WaitOptions, WorkspaceInputs, WorkspaceLifetime, WorkspaceOrigin, WorkspacePurpose, WorkspaceView, } from './client.js';
19
- export type { FinishedOperation, LifecycleOptions, ResumeOptions, WaitedLifecycleOptions, WaitedResumeOptions } from './lifecycle.js';
20
- export { formatTiming } from './progress.js';
21
- export type { LifecycleAction, LifecyclePhase, LifecycleTiming, ProgressEvent, ProgressListener, RetryRecord, ServerTiming, TimingOutcome, TimingPhase, } from './progress.js';
18
+ export type { AgentSession, AllocationMode, BillingCatalog, BillingSubscription, Caps, CheckoutSession, DiskLayout, Entitlements, FindByKeyOptions, ForkTarget, Invoice, InvoicePage, LifetimeFilter, ListParams, Me, OpenParams, OpenResponse, Operation, Page, PortalSession, PurposeFilter, ResetWorkspaceBody, ResumeAnswer, ResumeRequestOptions, ResumeResponse, ShardfluxOptions, SuspendRequest, SuspendWhenIdleOptions, SuspendWhenIdleResponse, SuspendWhenIdleResult, WaitOptions, WorkspaceInputs, WorkspaceLifetime, WorkspaceMemory, WorkspaceOrigin, WorkspacePurpose, WorkspaceView, } from './client.js';
19
+ export type { FinishedOperation, ForkOptions, LifecycleOptions, ResumeOptions, SuspendOptions, WaitedForkOptions, WaitedLifecycleOptions, WaitedResumeOptions, WaitedSuspendOptions } from './lifecycle.js';
20
+ export { durabilityOf, formatTiming, isDurable, lostSuspendOf } from './progress.js';
21
+ export type { Durability, LostSuspend, LifecycleAction, LifecyclePhase, LifecycleTiming, ProgressEvent, ProgressListener, RetryRecord, ServerTiming, TimingOutcome, TimingPhase, } from './progress.js';
22
22
  export { FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH } from './feedback.js';
23
23
  export type { AccountFeedbackParams, FeedbackCategory, FeedbackContext, FeedbackReceipt, SendFeedbackParams } from './feedback.js';
24
24
  export { UsageApi } from './usage.js';
@@ -28,7 +28,7 @@ export { TemplateFileError, packDirectory, parseTemplateText, readTemplateFile }
28
28
  export type { PackedFile, YamlParser } from './template-file.js';
29
29
  export { tarEnd, tarHeader, tarPadding } from './tar.js';
30
30
  export type { TarEntry, TarEntryType } from './tar.js';
31
- export type { BuildFromFileEvent, BuildFromFileOptions, BuildFromFileResult, BuildFromRecipeOptions, BuilderAvailability, CreateDraftBody, CreateDraftParams, CreateTemplateBuildParams, CreateTestInstanceBody, CreateVersionTestInstanceBody, CreateVersionTestInstanceParams, DraftOpened, LocalUpload, DraftState, OpenTestInstanceParams, OrgTemplateStorage, PublishDraftBody, PublishDraftParams, PutUploadOptions, SaveAsTemplateBody, SaveAsTemplateParams, SaveAsTemplateResponse, TemplateBuild, TemplateBuildLogUrl, TemplateBuildRecipeV2, TemplateBuildRegistrationState, TemplateBuildState, TemplateCategory, TemplateDefaults, TemplateDefaultsInput, TemplateDetail, TemplateDiffChange, TemplateDiffEntry, TemplateDiffPage, TemplateDiffParams, TemplateDraft, TemplateDraftSummary, TemplateEgressDefault, TemplateFileEntry, TemplateFilePage, TemplateFilesParams, TemplateFilesSummary, TemplateInput, TemplateLanguage, TemplateLanguages, TemplateOwner, TemplatePackage, TemplatePackageEcosystem, TemplatePackagePage, TemplateRecipe, TemplateRecipeV2, TemplateRecipeV2File, TemplateService, TemplateSettings, TemplateSettingsInput, TemplateSource, TemplateStartCommand, TemplateStorage, TemplateStorageWarning, TemplateSummary, TemplateUpload, TemplateUploadRequest, TemplateUploadResponse, TemplateUploadResult, TemplateVersion, TemplateVersionRecipe, TemplateVersionState, UploadData, WaitForBuildOptions, WorkspaceStartup, } from './templates.js';
31
+ export type { BuildFromFileEvent, BuildFromFileOptions, BuildFromFileResult, BuildFromRecipeOptions, BuilderAvailability, CreateDraftBody, CreateDraftParams, CreateTemplateBuildParams, CreateTestInstanceBody, CreateVersionTestInstanceBody, CreateVersionTestInstanceParams, DraftOpened, LocalUpload, DraftState, OpenTestInstanceParams, OrgTemplateStorage, PublishDraftBody, PublishDraftParams, PutUploadOptions, SaveAsTemplateBody, SaveAsTemplateParams, SaveAsTemplateResponse, TemplateBuild, TemplateBuildLogUrl, TemplateBuildRecipeV2, TemplateBuildRegistrationState, TemplateBuildState, TemplateCategory, TemplateDefaults, TemplateDefaultsInput, TemplateDetail, TemplateDiffChange, TemplateDiffEntry, TemplateDiffPage, TemplateDiffParams, TemplateDraft, TemplateDraftSummary, TemplateEgressDefault, TemplateFileEntry, TemplateFilePage, TemplateFilesParams, TemplateFilesSummary, TemplateInput, TemplateLanguage, TemplateLanguages, TemplateOwner, TemplatePackage, TemplatePackageEcosystem, TemplatePackagePage, TemplateRecipe, TemplateRecipeV2, TemplateRecipeV2File, TemplateService, TemplateSettings, TemplateSettingsInput, TemplateSource, TemplateStartCommand, TemplateStorage, TemplateStorageWarning, TemplateSummary, TemplateUpload, TemplateUploadRequest, TemplateUploadResponse, TemplateUploadResult, TemplateVersion, TemplateVersionImmutable, TemplateVersionRecipe, TemplateVersionState, UploadData, WaitForBuildOptions, WorkspaceStartup, } from './templates.js';
32
32
  export { SecretsApi, WorkspaceSecrets } from './secrets.js';
33
33
  export type { BoundSecretStatus, CreateOrganizationSecretParams, CreateSecretParams, Secret, SecretAccessEvent, SecretPermissions, SecretScope, SecretVersion, UpdateSecretParams, WorkspaceSecretBindings, } from './secrets.js';
34
34
  export { EgressPolicyApi } from './egress.js';
@@ -42,7 +42,7 @@ export type { HintOptions, HintResult, WakeOptions } from './workspace.js';
42
42
  export { CellClient, DEFAULT_TRANSITION_TIMEOUT_MS, cellPath, ndjson } from './cell.js';
43
43
  export { EXECUTION_ID, ExecutionResult, newExecutionId } from './executions.js';
44
44
  export type { ExecutionChange, ExecutionError, ExecutionGetOptions, ExecutionResultBody, ExecutionRunOptions, ExecutionState } from './executions.js';
45
- export type { TreeRevisionOptions, BrowserContent, BrowserContentRequest, BrowserScreenshotRequest, CellClientOptions, ExecSession, ExecStartRequest, FileEdit, FileInfo, FileList, FilePatchEdit, FilePatchParams, FilePatchRequest, FilePatchResult, FileReadResult, FileRevision, FileSearchMatch, FileSearchOptions, FileSearchRequest, FileSearchResponse, FileSearchResult, FileWriteResult, GitResult, GitStatus, OutputEvent, ProcessList, PtyOpenRequest, PtySession, Residency, RunOptions, RunResult, ServedFrom, Signal, WakeHintResult, WorkspaceChange, WorkspaceChangeKind, WorkspaceChangesPage, WorkspaceChangesParams, WorkspaceChangesSummary, } from './cell.js';
45
+ export type { TreeRevisionOptions, BrowserContent, BrowserContentRequest, BrowserScreenshotRequest, CellClientOptions, ExecSession, ExecStartRequest, MemoryGrow, BurstSummary, BurstError, FileEdit, FileInfo, FileList, FilePatchEdit, FilePatchParams, FilePatchRequest, FilePatchResult, FileReadResult, FileRevision, FileSearchMatch, FileSearchOptions, FileSearchRequest, FileSearchResponse, FileSearchResult, FileWriteResult, GitResult, GitStatus, OutputEvent, ProcessList, PtyOpenRequest, PtySession, Residency, RunOptions, RunResult, ServedFrom, Signal, WakeHintResult, WorkspaceChange, WorkspaceChangeKind, WorkspaceChangesPage, WorkspaceChangesParams, WorkspaceChangesSummary, } from './cell.js';
46
46
  export { ToolTokenManager } from './tokens.js';
47
47
  export type { ToolName, ToolToken, ToolTokenOptions } from './tokens.js';
48
48
  export { ToolArgumentError, executeToolCall, toAnthropicTools, toOpenAITools, validateArgs, workspaceTools } from './tools.js';
@@ -50,10 +50,15 @@ export type { AnthropicToolDefinition, JsonSchema, OpenAIChatToolDefinition, Ope
50
50
  export { CaptureError, ToolCallCapture, captureTool } from './capture.js';
51
51
  export type { CallLike, CallRef, CaptureCall, CaptureErrorKind, CaptureEvent, CaptureFlushResult, CapturePart, CaptureSelector, CaptureSource, CaptureStats, CaptureStatus, DropReason, ToolCallCaptureOptions, WrapOptions, } from './capture.js';
52
52
  export type { AiSdkAdapter, AiSdkToolEndEvent, AnthropicAdapter, ClaudeAdapter, ClaudeCaptureHooks, ClaudeHookCallback, ClaudeHookMatcher, LangChainAdapter, LangChainToolHandler, MastraAdapter, MastraAfterToolCallContext, McpAdapter, OpenAIAgentsAdapter, } from './capture-adapters.js';
53
- export { ExecStartError, NotSupportedForModeError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TreeRevisionMismatchError } from './errors.js';
53
+ export { DurabilityLostError, ExecStartError, NotSupportedForModeError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TreeRevisionMismatchError } from './errors.js';
54
54
  export type { AppErrorCode, CellErrorCode, ErrorCode, ErrorReason, KnownErrorReason, WorkspaceMode } from './errors.js';
55
55
  export { SDK_VERSION } from './http.js';
56
56
  export { API_KEY_TOOL_PERMISSIONS, AccountApiKeysApi, AccountAuditApi, AccountAuthApi, AccountBillingApi, AccountExportsApi, AccountInvitationsApi, AccountMembersApi, AccountOrganizationsApi, AccountProjectsApi, AccountTemplatesApi, AccountTotpApi, AccountUserApi, CheckoutTimeoutError, OrganizationExportsApi, SESSION_TOKEN_PATTERN, ShardfluxAccount, isSessionToken, parseEmailToken, } from './account.js';
57
57
  export type { AccountClientOptions, AccountDeletion, AccountDeletionCanceled, AccountDeletionScheduled, ApiKey, ApiKeyPage, ApiKeyToolPermission, AuditExportParams, AuthSessionInfo, AuthSessionPage, AuthSessionState, AuthUser, BillingInvoicePage, BillingPortalSession, CheckoutStatus, CreateApiKeyParams, CreatedApiKey, DataExport, EmailChangeConfirmResult, EmailChangeResult, Invitation, InvitationPage, LoginResult, Member, MemberPage, MemberRole, MfaChallengeResult, Organization, OrganizationDeletion, OrganizationDeletionResult, OrganizationPage, OrganizationRole, OrganizationWorkspacePage, OrganizationWorkspacesParams, PageParams, PasswordChangeResult, PasswordResetConfirmResult, PasswordResetRequestResult, Project, ProjectPage, RecoveryCodes, RegisterResult, ResendVerificationResult, SessionTokenUpdate, ShardfluxAccountOptions, SpendPolicyUpdate, StepUpResult, TotpConfirmResult, TotpDisableResult, TotpEnrollment, VerifyEmailResult, WaitForCheckoutOptions, } from './account.js';
58
58
  export { checkClientVersion, clientVersionStatus, compareVersions, versionCheckDisabledByEnv } from './version-check.js';
59
59
  export type { CheckClientVersionOptions, ClientEcosystem, ClientVersionEntry, ClientVersionStatus, ClientVersionStatusKind, ClientVersions, VersionCheckIdentity, VersionCheckOption, } from './version-check.js';
60
+ export { isWorkspaceGone } from './errors.js';
61
+ export { defaultFetch } from './http.js';
62
+ export type { IdlePolicy } from './client.js';
63
+ export type { IdleStatus, KeepaliveResult } from './cell.js';
64
+ export type { ExecInputResult } from './cell.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { BillingApi, Shardflux, WorkspacesApi, fetchBillingCatalog, pickByKey } from "./client.js";
2
- export { formatTiming } from "./progress.js";
2
+ export { durabilityOf, formatTiming, isDurable, lostSuspendOf } from "./progress.js";
3
3
  export { FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH } from "./feedback.js";
4
4
  export { UsageApi } from "./usage.js";
5
5
  export { TemplateBuildTimeoutError, TemplateBuildsApi, TemplateDraftApi, TemplatePackagesApi, TemplateUploadError, TemplateUploadsApi, TemplateVersionTestInstancesApi, TemplateVersionsApi, TemplatesApi, buildSettled, saveAsTemplateBody, } from "./templates.js";
@@ -15,7 +15,9 @@ export { EXECUTION_ID, ExecutionResult, newExecutionId } from "./executions.js";
15
15
  export { ToolTokenManager } from "./tokens.js";
16
16
  export { ToolArgumentError, executeToolCall, toAnthropicTools, toOpenAITools, validateArgs, workspaceTools } from "./tools.js";
17
17
  export { CaptureError, ToolCallCapture, captureTool } from "./capture.js";
18
- export { ExecStartError, NotSupportedForModeError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TreeRevisionMismatchError } from "./errors.js";
18
+ export { DurabilityLostError, ExecStartError, NotSupportedForModeError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TreeRevisionMismatchError } from "./errors.js";
19
19
  export { SDK_VERSION } from "./http.js";
20
20
  export { API_KEY_TOOL_PERMISSIONS, AccountApiKeysApi, AccountAuditApi, AccountAuthApi, AccountBillingApi, AccountExportsApi, AccountInvitationsApi, AccountMembersApi, AccountOrganizationsApi, AccountProjectsApi, AccountTemplatesApi, AccountTotpApi, AccountUserApi, CheckoutTimeoutError, OrganizationExportsApi, SESSION_TOKEN_PATTERN, ShardfluxAccount, isSessionToken, parseEmailToken, } from "./account.js";
21
21
  export { checkClientVersion, clientVersionStatus, compareVersions, versionCheckDisabledByEnv } from "./version-check.js";
22
+ export { isWorkspaceGone } from "./errors.js";
23
+ export { defaultFetch } from "./http.js";
@@ -32,6 +32,22 @@ export interface LifecycleOptions {
32
32
  export type WaitedLifecycleOptions = LifecycleOptions & {
33
33
  wait: true | WaitOptions;
34
34
  };
35
+ /**
36
+ * `suspend()` options (0.12.0+). A waited suspend resolves as soon as the workspace is sealed on its host, typically in a
37
+ * few hundred ms; the finished operation's `result.durable` turns true when the copy lands in durable storage,
38
+ * typically within a second. `durable: true` resolves only then: it waits for the suspend (so it implies `wait`), then
39
+ * for `result.durable`, within the same `timeoutMs` and `signal`. It throws DurabilityLostError if the copy cannot be
40
+ * made, and OperationTimeoutError (`durable: true`) when the time runs out first; the copy continues server side.
41
+ */
42
+ export interface SuspendOptions extends LifecycleOptions {
43
+ durable?: boolean;
44
+ }
45
+ /** Suspend options that wait: `wait` or `durable`. */
46
+ export type WaitedSuspendOptions = SuspendOptions & ({
47
+ wait: true | WaitOptions;
48
+ } | {
49
+ durable: true;
50
+ });
35
51
  /**
36
52
  * `resume()` options (0.9.0). With `wait`, the server holds the resume until the workspace runs and returns a tool token
37
53
  * with it: `agentLabel` and `tools` choose that token (defaults: a workspace handle's own, those given
@@ -45,6 +61,11 @@ export interface ResumeOptions extends LifecycleOptions {
45
61
  export type WaitedResumeOptions = ResumeOptions & {
46
62
  wait: true | WaitOptions;
47
63
  };
64
+ /** A waited fork (0.13.0+) returns the target view and token together when the API supports it. */
65
+ export type ForkOptions = ResumeOptions;
66
+ export type WaitedForkOptions = ForkOptions & {
67
+ wait: true | WaitOptions;
68
+ };
48
69
  /** Internal: the trace a wait continues instead of starting its own (open(), wake() and waited lifecycle calls). */
49
70
  export declare const TRACE: unique symbol;
50
71
  /** Internal: work a workspace handle does after the operation finished, inside the same trace (refreshing its view). */
@@ -63,6 +84,7 @@ export type InternalWaitOptions = WaitOptions & {
63
84
  [TRACE]?: Trace;
64
85
  };
65
86
  export type InternalLifecycleOptions = ResumeOptions & {
87
+ durable?: boolean;
66
88
  [AFTER_WAIT]?: (trace: Trace, operation: Operation) => Promise<void>;
67
89
  [HELD_RESUME]?: HeldResumeTarget;
68
90
  };
@@ -71,6 +93,7 @@ export declare function waitOptionsOf(opts: LifecycleOptions): WaitOptions | nul
71
93
  * Starts a lifecycle operation with `start` and, when `opts.wait` asks for it, waits for it to finish. The trace covers
72
94
  * the request, every observed state, and `AFTER_WAIT`; it ends with `done` either way.
73
95
  */
74
- export declare function runLifecycle(ctx: ClientContext, kind: LifecycleAction, workspaceId: string, start: (init: Pick<RequestOptions, 'onRetry'>) => Promise<Operation>, opts: InternalLifecycleOptions, capture?: {
96
+ export declare function runLifecycle(ctx: ClientContext, kind: LifecycleAction, workspaceId: string, start: (init: Pick<RequestOptions, 'onRetry'>, trace: Trace) => Promise<Operation>, opts: InternalLifecycleOptions, capture?: {
75
97
  settle?: boolean;
98
+ requestReason?: string | null;
76
99
  }): Promise<Operation>;