@shardflux/sdk 0.13.0 → 0.14.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.
@@ -191,7 +191,7 @@ export interface paths {
191
191
  * Client messages are JSON `ExecClientMessage`. Close codes: 1000 after
192
192
  * the `exit` event, 1001 on gateway shutdown (reconnect with offsets),
193
193
  * 4401 unauthenticated, 4403 forbidden, 4409 stale epoch / workspace
194
- * busy / workspace not running, 4410 workspace gone, 4429 rate limited,
194
+ * busy / workspace not running, 4410 workspace gone, 4429 rate limited / quota exceeded,
195
195
  * 4500/4503/4504 internal/unavailable/timeout (4000 + HTTP status; see
196
196
  * "WebSocket close codes" above; the close reason is compact JSON with
197
197
  * the error `code`). A refused upgrade from an allowed Origin is
@@ -265,14 +265,18 @@ export interface paths {
265
265
  get?: never;
266
266
  put?: never;
267
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`
268
+ * Stop the command and every process it started (SIGTERM, SIGKILL after grace)
269
+ * @description Sends SIGTERM to every process the command started, background and
270
+ * setsid processes included, and SIGKILL after `grace_ms` (0 to 60000;
271
+ * 0 or omitted is 5000). Answers when they have all exited: at once when
272
+ * SIGTERM ends them, otherwise after the grace and the SIGKILL. A lost
273
+ * command is stopped the same way; a command that already exited keeps
274
+ * its result and what it left running is stopped. `503
275
+ * dependency_unavailable` (retryable) while a process cannot exit yet.
276
+ * Template versions before python-node-browser 13, ubuntu-24.04 4,
277
+ * ubuntu-22.04 2 and debian-12 2 signal the command's process group and
278
+ * return an ended session as it is. A `grace_ms` outside the range is `422 validation_failed`
274
279
  * (`details.field` `grace_ms`, `details.minimum`, `details.maximum`).
275
- * An ended session is returned as it is.
276
280
  */
277
281
  post: operations["execCancel"];
278
282
  delete?: never;
@@ -876,7 +880,7 @@ export type webhooks = Record<string, never>;
876
880
  export interface components {
877
881
  schemas: {
878
882
  /** @enum {string} */
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";
883
+ 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" | "quota_exceeded";
880
884
  ErrorBody: {
881
885
  error: {
882
886
  code: components["schemas"]["ErrorCode"];
@@ -949,6 +953,12 @@ export interface components {
949
953
  burst_vcpus?: number;
950
954
  /** @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
955
  burst_memory_mib?: number;
956
+ /**
957
+ * @description Contracts §41.1: whether an elastic workspace's memory grows before the command starts. `heavy` grows it to the exec-start size first; `light` starts at once (pressure grows cover a spike); `auto` (default) grows only for a heavy command family (package installers, test runners, compilers, type checkers, bundlers). Changes only elastic workspaces in memory layout v2; any other value is 422 validation_failed (details.field resource_hint).
958
+ * @default auto
959
+ * @enum {string}
960
+ */
961
+ resource_hint?: "auto" | "light" | "heavy";
952
962
  };
953
963
  /** @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). */
954
964
  SecretRefs: string[];
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.13.0";
2
+ export declare const SDK_VERSION = "0.14.0";
3
3
  export interface RequestOptions {
4
4
  query?: Record<string, string | number | boolean | undefined | null>;
5
5
  json?: unknown;
@@ -10,6 +10,11 @@ export interface RequestOptions {
10
10
  idempotencyKey?: string;
11
11
  /** The request has no effect a retry could duplicate (a read-only POST such as files/search): retried like a GET. */
12
12
  idempotent?: boolean;
13
+ /**
14
+ * false: the cell gateway's 429 `quota_exceeded` surfaces at once instead of being retried (the wake hint, which
15
+ * never waits). Default true.
16
+ */
17
+ quotaRetry?: boolean;
13
18
  signal?: AbortSignal;
14
19
  /** Override the client's default request timeout (ms); 0 disables it (streams). */
15
20
  timeoutMs?: number;
@@ -33,13 +38,45 @@ export interface HttpOptions {
33
38
  onSuccess?: (() => void) | undefined;
34
39
  }
35
40
  export declare const defaultSleep: (ms: number) => Promise<void>;
41
+ /**
42
+ * How long an idle pooled connection stays reusable (ms): 5 minutes, instead of undici's 4 s default, so the request
43
+ * after an agent's usual pause between tool calls (15 s to a few minutes) skips a new TCP and TLS handshake. Far below
44
+ * the 3600 s idle timeout of the load balancer in front of the API and the cell endpoints, so the server side never
45
+ * closes a connection for idleness while the client still treats it as reusable; below the AWS NAT gateway's 350 s idle
46
+ * timeout; and the same limit Chrome keeps for used idle sockets. Also caps a server's `Keep-Alive: timeout` hint.
47
+ */
48
+ export declare const KEEP_ALIVE_TIMEOUT_MS = 300000;
49
+ /**
50
+ * TCP keepalive probes start after a pooled socket has been idle this long (ms), so NATs and firewalls with shorter
51
+ * idle timeouts than the pool's (Azure SNAT: 4 minutes) keep the connection instead of silently dropping it. Equal to
52
+ * undici's own default, set explicitly so it is visible and tested.
53
+ */
54
+ export declare const TCP_KEEPALIVE_INITIAL_DELAY_MS = 60000;
55
+ /** The private pool's undici Agent options (exported for tests). */
56
+ export declare const POOL_OPTIONS: {
57
+ readonly allowH2: false;
58
+ readonly pipelining: 1;
59
+ readonly keepAliveTimeout: 300000;
60
+ readonly keepAliveMaxTimeout: 300000;
61
+ readonly connect: {
62
+ readonly keepAlive: true;
63
+ readonly keepAliveInitialDelay: 60000;
64
+ };
65
+ };
36
66
  /**
37
67
  * 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.
68
+ * private HTTP/1.1-only dispatcher whose idle connections stay reusable for KEEP_ALIVE_TIMEOUT_MS. Other runtimes
69
+ * retain native fetch. SHARDFLUX_HTTP_KEEPALIVE=0 forces close for diagnosis; =1 retains the historical opt-in to
70
+ * native pooling. An explicitly supplied fetch is untouched. Idle pooled sockets do not keep the process alive.
40
71
  */
41
72
  export declare function defaultFetch(env?: Record<string, string | undefined> | undefined, undiciVersion?: string | undefined): typeof fetch;
42
73
  export declare function buildUrl(base: string, path: string, query?: RequestOptions['query']): string;
74
+ /**
75
+ * True when fetch failed because its connection closed or reset before a response arrived: what a request sees when
76
+ * it went out on a pooled connection the other side closed while it sat idle. Connect failures (ECONNREFUSED,
77
+ * ENOTFOUND, UND_ERR_CONNECT_TIMEOUT), timeouts and aborts are not this.
78
+ */
79
+ export declare function isStaleConnectionError(err: unknown): boolean;
43
80
  /** `X-Tree-Revision` (file-first workspaces): a non-negative integer, else null. */
44
81
  export declare function treeRevisionOf(headers: Headers): number | null;
45
82
  /** Turns a non-2xx response into a ShardfluxApiError (or a protocol error for undocumented bodies). */
package/dist/http.js CHANGED
@@ -4,18 +4,44 @@
4
4
  * retries only where a retry cannot duplicate an effect (safe methods,
5
5
  * requests carrying an Idempotency-Key, and read-only POSTs marked
6
6
  * `idempotent`). A retryable 429/502/503/504 (e.g. 503 `host_capacity`, 503
7
- * `wake_failed`) waits `Retry-After` (at most 5 s) before the retry.
7
+ * `wake_failed`) waits `Retry-After` (at most 5 s) before the retry. Such a request whose connection closed before
8
+ * any response (a pooled connection gone stale while idle) is sent again at once, once, outside `maxRetries`. The cell
9
+ * gateway's 429 `quota_exceeded` (every working slot of the plan is taken) is retried for any request (0.14.0+): it is
10
+ * refused at admission, so nothing ran.
8
11
  */
9
- import { ShardfluxApiError, ShardfluxProtocolError, apiError, isErrorBody } from "./errors.js";
12
+ import { ShardfluxApiError, ShardfluxProtocolError, apiError, isErrorBody, isWorkingQuotaRefusal } from "./errors.js";
10
13
  import { describeFailure } from "./progress.js";
11
- export const SDK_VERSION = '0.13.0';
14
+ export const SDK_VERSION = '0.14.0';
12
15
  export const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
16
+ /**
17
+ * How long an idle pooled connection stays reusable (ms): 5 minutes, instead of undici's 4 s default, so the request
18
+ * after an agent's usual pause between tool calls (15 s to a few minutes) skips a new TCP and TLS handshake. Far below
19
+ * the 3600 s idle timeout of the load balancer in front of the API and the cell endpoints, so the server side never
20
+ * closes a connection for idleness while the client still treats it as reusable; below the AWS NAT gateway's 350 s idle
21
+ * timeout; and the same limit Chrome keeps for used idle sockets. Also caps a server's `Keep-Alive: timeout` hint.
22
+ */
23
+ export const KEEP_ALIVE_TIMEOUT_MS = 300_000;
24
+ /**
25
+ * TCP keepalive probes start after a pooled socket has been idle this long (ms), so NATs and firewalls with shorter
26
+ * idle timeouts than the pool's (Azure SNAT: 4 minutes) keep the connection instead of silently dropping it. Equal to
27
+ * undici's own default, set explicitly so it is visible and tested.
28
+ */
29
+ export const TCP_KEEPALIVE_INITIAL_DELAY_MS = 60_000;
30
+ /** The private pool's undici Agent options (exported for tests). */
31
+ export const POOL_OPTIONS = {
32
+ allowH2: false,
33
+ pipelining: 1,
34
+ keepAliveTimeout: KEEP_ALIVE_TIMEOUT_MS,
35
+ keepAliveMaxTimeout: KEEP_ALIVE_TIMEOUT_MS,
36
+ connect: { keepAlive: true, keepAliveInitialDelay: TCP_KEEPALIVE_INITIAL_DELAY_MS },
37
+ };
13
38
  /** One private HTTP/1.1 pool on Node 26+, shared by SDK clients. No global dispatcher changes. */
14
39
  let nodeFetch;
15
40
  /**
16
41
  * 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.
42
+ * private HTTP/1.1-only dispatcher whose idle connections stay reusable for KEEP_ALIVE_TIMEOUT_MS. Other runtimes
43
+ * retain native fetch. SHARDFLUX_HTTP_KEEPALIVE=0 forces close for diagnosis; =1 retains the historical opt-in to
44
+ * native pooling. An explicitly supplied fetch is untouched. Idle pooled sockets do not keep the process alive.
19
45
  */
20
46
  export function defaultFetch(env = globalThis.process?.env, undiciVersion = globalThis.process?.versions?.undici) {
21
47
  // Why a fresh connection on Node 26 (its bundled HTTP client, measured): docs/progress/startup-latency.md.
@@ -30,7 +56,7 @@ export function defaultFetch(env = globalThis.process?.env, undiciVersion = glob
30
56
  return base;
31
57
  return async (input, init) => {
32
58
  nodeFetch ??= import('undici').then(({ Agent, fetch: pooledFetch }) => {
33
- const dispatcher = new Agent({ allowH2: false, pipelining: 1 });
59
+ const dispatcher = new Agent({ ...POOL_OPTIONS, connect: { ...POOL_OPTIONS.connect } });
34
60
  return async (input, init) => {
35
61
  const response = await pooledFetch(input, { ...init, dispatcher });
36
62
  // Web-standard runtime shape; undici and DOM iterator declarations differ.
@@ -49,6 +75,25 @@ export function buildUrl(base, path, query) {
49
75
  }
50
76
  return u.toString();
51
77
  }
78
+ /** Codes (on fetch's `cause` chain) of a connection that closed or reset before the response arrived. */
79
+ const STALE_CONNECTION_CODES = new Set(['UND_ERR_SOCKET', 'ECONNRESET', 'EPIPE', 'UND_ERR_CLOSED']);
80
+ /**
81
+ * True when fetch failed because its connection closed or reset before a response arrived: what a request sees when
82
+ * it went out on a pooled connection the other side closed while it sat idle. Connect failures (ECONNREFUSED,
83
+ * ENOTFOUND, UND_ERR_CONNECT_TIMEOUT), timeouts and aborts are not this.
84
+ */
85
+ export function isStaleConnectionError(err) {
86
+ if (!(err instanceof TypeError))
87
+ return false;
88
+ const seen = new Set();
89
+ for (let c = err.cause; typeof c === 'object' && c !== null && !seen.has(c); c = c.cause) {
90
+ seen.add(c);
91
+ const code = c.code;
92
+ if (typeof code === 'string' && STALE_CONNECTION_CODES.has(code))
93
+ return true;
94
+ }
95
+ return false;
96
+ }
52
97
  function retryAfterSeconds(res) {
53
98
  const v = res.headers.get('retry-after');
54
99
  if (v === null)
@@ -89,6 +134,9 @@ export class HttpClient {
89
134
  const safe = method === 'GET' || method === 'HEAD';
90
135
  const retriable = safe || init.idempotencyKey !== undefined || init.idempotent === true;
91
136
  const sleep = this.opts.sleep ?? defaultSleep;
137
+ // Retries counted against maxRetries (they also set the backoff); `attempt` counts every send.
138
+ let counted = 0;
139
+ let staleReplayed = false;
92
140
  for (let attempt = 0;; attempt += 1) {
93
141
  const headers = {
94
142
  accept: init.accept ?? 'application/json',
@@ -122,9 +170,20 @@ export class HttpClient {
122
170
  catch (err) {
123
171
  if (init.signal?.aborted)
124
172
  throw err;
125
- if (!retriable || attempt >= this.opts.maxRetries)
173
+ if (!retriable)
174
+ throw err;
175
+ if (!staleReplayed && !signal?.aborted && isStaleConnectionError(err)) {
176
+ // The connection closed before any response, typically a pooled one the server or a NAT closed while idle:
177
+ // send again at once on a fresh connection, once per request and outside maxRetries (as Go's net/http
178
+ // replays a request that failed on a reused connection when it is idempotent or carries an Idempotency-Key).
179
+ staleReplayed = true;
180
+ init.onRetry?.({ request: `${method} ${path}`, attempt: attempt + 1, cause: describeFailure(err), delayMs: 0 });
181
+ continue;
182
+ }
183
+ if (counted >= this.opts.maxRetries)
126
184
  throw err;
127
- const delayMs = Math.min(2_000, 200 * 2 ** attempt);
185
+ const delayMs = Math.min(2_000, 200 * 2 ** counted);
186
+ counted += 1;
128
187
  init.onRetry?.({ request: `${method} ${path}`, attempt: attempt + 1, cause: describeFailure(err, headersAbort?.signal.aborted ? headersMs : timeoutMs), delayMs });
129
188
  await sleep(delayMs);
130
189
  continue;
@@ -146,10 +205,15 @@ export class HttpClient {
146
205
  const error = await errorFrom(res, this.opts.source);
147
206
  const retryableStatus = res.status === 429 || res.status === 502 || res.status === 503 || res.status === 504;
148
207
  const retryableError = error instanceof ShardfluxApiError ? error.retryable && retryableStatus : retryableStatus;
149
- if (!retriable || !retryableError || attempt >= this.opts.maxRetries)
208
+ // The working-at-once quota (cell 429 quota_exceeded): the gateway refuses the call at admission, before the
209
+ // workspace is touched, so nothing ran and even a non-idempotent request (exec start, stdin, PTY create,
210
+ // keepalive) is safe to send again. Same Retry-After wait and retry budget as every other retry.
211
+ const admissionRefusal = init.quotaRetry !== false && isWorkingQuotaRefusal(error);
212
+ if ((!retriable && !admissionRefusal) || !retryableError || counted >= this.opts.maxRetries)
150
213
  throw error;
151
214
  const hinted = error instanceof ShardfluxApiError ? error.retryAfterSeconds : undefined;
152
- const delayMs = Math.min(5_000, hinted !== undefined ? hinted * 1000 : 200 * 2 ** attempt);
215
+ const delayMs = Math.min(5_000, hinted !== undefined ? hinted * 1000 : 200 * 2 ** counted);
216
+ counted += 1;
153
217
  init.onRetry?.({ request: `${method} ${path}`, attempt: attempt + 1, cause: describeFailure(error), delayMs });
154
218
  await sleep(delayMs);
155
219
  }
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, 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';
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, ResizeAppliesAt, ResizeCpu, ResizeDeferReason, ResizeDisk, ResizeLimitReason, ResizeMemory, ResizeParams, ResizeResult, ResizeResponse, ResumeAnswer, ResumeRequestOptions, ResumeResponse, ShardfluxOptions, SuspendRequest, SuspendWhenIdleOptions, SuspendWhenIdleResponse, SuspendWhenIdleResult, WaitOptions, WorkspaceInputs, WorkspaceLifetime, WorkspaceMemory, WorkspaceOrigin, WorkspacePurpose, WorkspaceView, } from './client.js';
19
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';
20
+ export { COLD_BOOT_REASONS, durabilityOf, formatTiming, hostLostOf, isDurable, lostSuspendOf } from './progress.js';
21
+ export type { ColdBootReason, Durability, HostLost, 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';
@@ -29,6 +29,8 @@ 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
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
+ export { WorkspacePorts } from './ports.js';
33
+ export type { ExposePortResult, ExposedPort, PortCallbackResponse, PortCallbackUrl, PortLink, PortLinkOptions, PortLinkResponse, PortToken, PortTokenOptions, PortTokenResponse, PortView } from './ports.js';
32
34
  export { SecretsApi, WorkspaceSecrets } from './secrets.js';
33
35
  export type { BoundSecretStatus, CreateOrganizationSecretParams, CreateSecretParams, Secret, SecretAccessEvent, SecretPermissions, SecretScope, SecretVersion, UpdateSecretParams, WorkspaceSecretBindings, } from './secrets.js';
34
36
  export { EgressPolicyApi } from './egress.js';
@@ -42,7 +44,7 @@ export type { HintOptions, HintResult, WakeOptions } from './workspace.js';
42
44
  export { CellClient, DEFAULT_TRANSITION_TIMEOUT_MS, cellPath, ndjson } from './cell.js';
43
45
  export { EXECUTION_ID, ExecutionResult, newExecutionId } from './executions.js';
44
46
  export type { ExecutionChange, ExecutionError, ExecutionGetOptions, ExecutionResultBody, ExecutionRunOptions, ExecutionState } from './executions.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';
47
+ 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, ResourceHint, RunOptions, RunResult, ServedFrom, Signal, WakeHintResult, WorkspaceChange, WorkspaceChangeKind, WorkspaceChangesPage, WorkspaceChangesParams, WorkspaceChangesSummary, } from './cell.js';
46
48
  export { ToolTokenManager } from './tokens.js';
47
49
  export type { ToolName, ToolToken, ToolTokenOptions } from './tokens.js';
48
50
  export { ToolArgumentError, executeToolCall, toAnthropicTools, toOpenAITools, validateArgs, workspaceTools } from './tools.js';
@@ -54,7 +56,9 @@ export { DurabilityLostError, ExecStartError, NotSupportedForModeError, Operatio
54
56
  export type { AppErrorCode, CellErrorCode, ErrorCode, ErrorReason, KnownErrorReason, WorkspaceMode } from './errors.js';
55
57
  export { SDK_VERSION } from './http.js';
56
58
  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
- 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';
59
+ export { codexIdentityProof } from './codex-proof.js';
60
+ export type { CodexIdentityProof, CodexProofOptions, CodexProofUnavailable } from './codex-proof.js';
61
+ 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, SignupResult, SignupAccess, EmailCodeResult, ResendVerificationResult, SessionTokenUpdate, ShardfluxAccountOptions, SpendPolicyUpdate, StepUpResult, TotpConfirmResult, TotpDisableResult, TotpEnrollment, VerifyEmailResult, WaitForCheckoutOptions, } from './account.js';
58
62
  export { checkClientVersion, clientVersionStatus, compareVersions, versionCheckDisabledByEnv } from './version-check.js';
59
63
  export type { CheckClientVersionOptions, ClientEcosystem, ClientVersionEntry, ClientVersionStatus, ClientVersionStatusKind, ClientVersions, VersionCheckIdentity, VersionCheckOption, } from './version-check.js';
60
64
  export { isWorkspaceGone } from './errors.js';
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  export { BillingApi, Shardflux, WorkspacesApi, fetchBillingCatalog, pickByKey } from "./client.js";
2
- export { durabilityOf, formatTiming, isDurable, lostSuspendOf } from "./progress.js";
2
+ export { COLD_BOOT_REASONS, durabilityOf, formatTiming, hostLostOf, 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";
6
6
  export { TemplateFileError, packDirectory, parseTemplateText, readTemplateFile } from "./template-file.js";
7
7
  export { tarEnd, tarHeader, tarPadding } from "./tar.js";
8
+ export { WorkspacePorts } from "./ports.js";
8
9
  export { SecretsApi, WorkspaceSecrets } from "./secrets.js";
9
10
  export { EgressPolicyApi } from "./egress.js";
10
11
  export { VolumesApi } from "./volumes.js";
@@ -18,6 +19,7 @@ export { CaptureError, ToolCallCapture, captureTool } from "./capture.js";
18
19
  export { DurabilityLostError, ExecStartError, NotSupportedForModeError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TreeRevisionMismatchError } from "./errors.js";
19
20
  export { SDK_VERSION } from "./http.js";
20
21
  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";
22
+ export { codexIdentityProof } from "./codex-proof.js";
21
23
  export { checkClientVersion, clientVersionStatus, compareVersions, versionCheckDisabledByEnv } from "./version-check.js";
22
24
  export { isWorkspaceGone } from "./errors.js";
23
25
  export { defaultFetch } from "./http.js";
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Inbound ports (0.14.0; contracts §39.3): serve a TCP port of a processful workspace at its own HTTPS URL,
3
+ * `https://<port>-<handle>.<ingress domain>`. Every port is private: a request carries a port token
4
+ * (`Authorization: Bearer sfp_…`), a signed link's browser session, or arrives on the port's callback URL. A request
5
+ * to a suspended or parked workspace wakes it and is served once it runs.
6
+ *
7
+ * const { url } = await workspace.ports.expose(3000);
8
+ * const { token } = await workspace.ports.token(3000);
9
+ * await fetch(url, { headers: { authorization: `Bearer ${token}` } });
10
+ *
11
+ * Types are written by hand (checked against the generated contract in type-checks.ts).
12
+ */
13
+ import type { components, operations } from './generated/app-api.js';
14
+ import type { ClientContext } from './client.js';
15
+ type Created<Op> = Op extends {
16
+ responses: {
17
+ 201: {
18
+ content: {
19
+ 'application/json': infer T;
20
+ };
21
+ };
22
+ };
23
+ } ? T : never;
24
+ /** An exposed port as the API sends it (`Port`). */
25
+ export type PortView = components['schemas']['Port'];
26
+ /** POST …/ports/{port}/tokens (201). */
27
+ export type PortTokenResponse = Created<operations['postV1WorkspacesWorkspaceIdPortsPortTokens']>;
28
+ /** POST …/ports/{port}/links (201). */
29
+ export type PortLinkResponse = Created<operations['postV1WorkspacesWorkspaceIdPortsPortLinks']>;
30
+ /** POST …/ports/{port}/callback (201). */
31
+ export type PortCallbackResponse = Created<operations['postV1WorkspacesWorkspaceIdPortsPortCallback']>;
32
+ /** An exposed port of a workspace. */
33
+ export interface ExposedPort {
34
+ /** TCP port inside the workspace (1-65535). */
35
+ port: number;
36
+ /** `https://<port>-<handle>.<ingress domain>`: requests need a port token, a link's browser session or the callback URL. */
37
+ url: string;
38
+ /** When the port was exposed (RFC 3339). Exposing it again after a close starts a new exposure. */
39
+ createdAt: string;
40
+ /** Set when the port has a callback URL (its secret is shown only by `createCallbackUrl()`). */
41
+ callback: {
42
+ createdAt: string;
43
+ } | null;
44
+ }
45
+ /** `expose()`: the port, and whether this call exposed it (201) or it already was (200, unchanged). */
46
+ export interface ExposePortResult extends ExposedPort {
47
+ created: boolean;
48
+ }
49
+ /** A port token: send it as `Authorization: Bearer <token>` (or `X-Shardflux-Token`) to `url`. */
50
+ export interface PortToken {
51
+ /** `sfp_…`. Revoked when the port is closed, the workspace is deleted or the API key that minted it is revoked. */
52
+ token: string;
53
+ /** RFC 3339. */
54
+ expiresAt: string;
55
+ /** The port's URL. */
56
+ url: string;
57
+ }
58
+ /** A signed link: opening it in a browser starts a session for the port (a cookie until `expiresAt`) and lands on its path. */
59
+ export interface PortLink {
60
+ url: string;
61
+ /** RFC 3339: the link and the browser session it starts end then. */
62
+ expiresAt: string;
63
+ }
64
+ /**
65
+ * The port's callback URL: register `url` plus your own path with GitHub, Slack, Stripe or an OAuth provider. A request
66
+ * to `<url><rest>` reaches `/<rest>` (query kept) without a token header and wakes the workspace like any request.
67
+ */
68
+ export interface PortCallbackUrl {
69
+ /** `https://<port>-<handle>.<domain>/__shardflux/callback/sfcb_…/`. Shown once: the API keeps only its hash. */
70
+ url: string;
71
+ /** RFC 3339. */
72
+ createdAt: string;
73
+ }
74
+ export interface PortTokenOptions {
75
+ /** Lifetime in seconds: 60-86400 (default 3600). */
76
+ ttlSeconds?: number;
77
+ }
78
+ export interface PortLinkOptions {
79
+ /** Lifetime of the link and of the browser session it starts, in seconds: 60-604800 (default 86400). */
80
+ ttlSeconds?: number;
81
+ /** Where the link lands: a path starting with one `/`, printable ASCII, query allowed (default `/`). */
82
+ path?: string;
83
+ }
84
+ export declare function exposedPortOf(view: PortView): ExposedPort;
85
+ /**
86
+ * The exposed ports of one workspace (`workspace.ports`, `cloud.workspaces.ports(id)`). Errors are ShardfluxApiError:
87
+ * 403 `forbidden` reason `inbound_ports_not_available` (inbound ports are not enabled for the organization), 404
88
+ * `not_found` reason `port_not_exposed` (tokens, links and callback URLs of a port that is not exposed), 409 `conflict`
89
+ * reason `port_limit` (at most 10 exposed ports, `details.limit`) or `workspace_deleted`, NotSupportedForModeError for a
90
+ * file-first workspace, 422 `validation_failed` for a port outside 1-65535 or an option out of range.
91
+ */
92
+ export declare class WorkspacePorts {
93
+ #private;
94
+ constructor(ctx: ClientContext, workspaceId: string);
95
+ /**
96
+ * Exposes TCP `port` at its own HTTPS URL (idempotent: a port already exposed is returned unchanged, `created`
97
+ * false, and its tokens, links and callback URL stay valid). The server inside the workspace must listen on
98
+ * 0.0.0.0 (all interfaces), not only on 127.0.0.1.
99
+ */
100
+ expose(port: number): Promise<ExposePortResult>;
101
+ /** The exposed ports, ordered by port. */
102
+ list(): Promise<ExposedPort[]>;
103
+ /**
104
+ * Closes the port (idempotent: also when it is not exposed). Its tokens, links and callback URL stop working at once,
105
+ * and stay revoked if the port is exposed again.
106
+ */
107
+ close(port: number): Promise<void>;
108
+ /** Mints a port token for an exposed port (`use` bearer): `Authorization: Bearer <token>` on requests to `url`. */
109
+ token(port: number, opts?: PortTokenOptions): Promise<PortToken>;
110
+ /** A signed link that opens the port in a browser. Share it like a password: anyone with it can open the port until it expires. */
111
+ link(port: number, opts?: PortLinkOptions): Promise<PortLink>;
112
+ /** Creates the port's callback URL, or replaces it (the previous one stops working). Its secret is shown only here. */
113
+ createCallbackUrl(port: number): Promise<PortCallbackUrl>;
114
+ /** Revokes the port's callback URL (idempotent while the port is exposed). */
115
+ revokeCallbackUrl(port: number): Promise<void>;
116
+ }
117
+ export {};
package/dist/ports.js ADDED
@@ -0,0 +1,62 @@
1
+ export function exposedPortOf(view) {
2
+ return { port: view.port, url: view.url, createdAt: view.created_at, callback: view.callback ? { createdAt: view.callback.created_at } : null };
3
+ }
4
+ /**
5
+ * The exposed ports of one workspace (`workspace.ports`, `cloud.workspaces.ports(id)`). Errors are ShardfluxApiError:
6
+ * 403 `forbidden` reason `inbound_ports_not_available` (inbound ports are not enabled for the organization), 404
7
+ * `not_found` reason `port_not_exposed` (tokens, links and callback URLs of a port that is not exposed), 409 `conflict`
8
+ * reason `port_limit` (at most 10 exposed ports, `details.limit`) or `workspace_deleted`, NotSupportedForModeError for a
9
+ * file-first workspace, 422 `validation_failed` for a port outside 1-65535 or an option out of range.
10
+ */
11
+ export class WorkspacePorts {
12
+ #ctx;
13
+ #workspaceId;
14
+ constructor(ctx, workspaceId) {
15
+ this.#ctx = ctx;
16
+ this.#workspaceId = workspaceId;
17
+ }
18
+ #path(port, rest = '') {
19
+ const base = `/v1/workspaces/${encodeURIComponent(this.#workspaceId)}/ports`;
20
+ return port === undefined ? base : `${base}/${encodeURIComponent(String(port))}${rest}`;
21
+ }
22
+ /**
23
+ * Exposes TCP `port` at its own HTTPS URL (idempotent: a port already exposed is returned unchanged, `created`
24
+ * false, and its tokens, links and callback URL stay valid). The server inside the workspace must listen on
25
+ * 0.0.0.0 (all interfaces), not only on 127.0.0.1.
26
+ */
27
+ async expose(port) {
28
+ const r = await this.#ctx.http.jsonWithStatus('PUT', this.#path(port), { idempotent: true }, this.#ctx.authorization);
29
+ return { ...exposedPortOf(r.body), created: r.status === 201 };
30
+ }
31
+ /** The exposed ports, ordered by port. */
32
+ async list() {
33
+ const body = await this.#ctx.http.json('GET', this.#path(), {}, this.#ctx.authorization);
34
+ return body.ports.map(exposedPortOf);
35
+ }
36
+ /**
37
+ * Closes the port (idempotent: also when it is not exposed). Its tokens, links and callback URL stop working at once,
38
+ * and stay revoked if the port is exposed again.
39
+ */
40
+ async close(port) {
41
+ await this.#ctx.http.json('DELETE', this.#path(port), { idempotent: true }, this.#ctx.authorization);
42
+ }
43
+ /** Mints a port token for an exposed port (`use` bearer): `Authorization: Bearer <token>` on requests to `url`. */
44
+ async token(port, opts = {}) {
45
+ const body = await this.#ctx.http.json('POST', this.#path(port, '/tokens'), { json: opts.ttlSeconds !== undefined ? { ttl_seconds: opts.ttlSeconds } : {} }, this.#ctx.authorization);
46
+ return { token: body.token, expiresAt: body.expires_at, url: body.url };
47
+ }
48
+ /** A signed link that opens the port in a browser. Share it like a password: anyone with it can open the port until it expires. */
49
+ async link(port, opts = {}) {
50
+ const body = await this.#ctx.http.json('POST', this.#path(port, '/links'), { json: { ...(opts.ttlSeconds !== undefined ? { ttl_seconds: opts.ttlSeconds } : {}), ...(opts.path !== undefined ? { path: opts.path } : {}) } }, this.#ctx.authorization);
51
+ return { url: body.url, expiresAt: body.expires_at };
52
+ }
53
+ /** Creates the port's callback URL, or replaces it (the previous one stops working). Its secret is shown only here. */
54
+ async createCallbackUrl(port) {
55
+ const body = await this.#ctx.http.json('POST', this.#path(port, '/callback'), {}, this.#ctx.authorization);
56
+ return { url: body.url, createdAt: body.created_at };
57
+ }
58
+ /** Revokes the port's callback URL (idempotent while the port is exposed). */
59
+ async revokeCallbackUrl(port) {
60
+ await this.#ctx.http.json('DELETE', this.#path(port, '/callback'), { idempotent: true }, this.#ctx.authorization);
61
+ }
62
+ }
@@ -18,7 +18,7 @@
18
18
  import type { components } from './generated/app-api.js';
19
19
  type Operation = components['schemas']['Operation'];
20
20
  /** The SDK call a trace follows. `wait` is a direct waitForOperation(); `token` a tool token fetched for tool calls. */
21
- export type LifecycleAction = 'open' | 'suspend' | 'resume' | 'snapshot' | 'fork' | 'delete' | 'close' | 'reset' | 'wake' | 'wait' | 'token';
21
+ export type LifecycleAction = 'open' | 'suspend' | 'resume' | 'snapshot' | 'fork' | 'delete' | 'close' | 'reset' | 'resize' | 'wake' | 'wait' | 'token';
22
22
  /**
23
23
  * - `request`: an API request that starts or joins the operation. A held open (`Prefer: wait`) spends the server's
24
24
  * hold here (reason `held`), so the states inside it show only in the server timing.
@@ -74,9 +74,10 @@ export interface ServerTiming {
74
74
  warmFallback: string | null;
75
75
  /**
76
76
  * `result.resume_path`: `local_cache`, `prestaged` (copied to this host ahead of the resume),
77
- * `download` (the checkpoint had to be fetched first), `cold_boot` (0.11.0+: the saved disk was booted after a
78
- * platform runtime change; processes restarted; see `memoryRestored`) or `reset_blank_layer` (the first start after a
79
- * reset).
77
+ * `download` (the checkpoint had to be fetched first), `cold_boot` (0.11.0+: the saved disk was booted instead of
78
+ * restoring memory, see `coldBootReason`; processes restarted; see `memoryRestored`), `reset_blank_layer` (the first
79
+ * start after a reset) or `thaw` (0.13.1+: resumed while its suspend was still being written; the same VM continued
80
+ * in place).
80
81
  */
81
82
  resumePath: string | null;
82
83
  /**
@@ -87,10 +88,12 @@ export interface ServerTiming {
87
88
  */
88
89
  memoryRestored?: boolean | null;
89
90
  /**
90
- * `result.cold_boot_reason` (0.11.0+), with `resumePath` `cold_boot`: why the memory could not be restored, e.g.
91
- * `runtime_changed` (the platform's VM runtime changed after the suspend). Null otherwise.
91
+ * `result.cold_boot_reason` (0.11.0+), with `resumePath` `cold_boot`: why the memory could not be restored:
92
+ * `runtime_changed` (the platform's VM runtime changed after the suspend), `host_lost` (0.13.1+: the machine the
93
+ * workspace ran on failed; it booted from its disk, files kept, see `hostLost`) or `runtime_retired` (0.14.0+: the
94
+ * runtime the workspace was suspended on was retired after an announced window). Null otherwise.
92
95
  */
93
- coldBootReason?: string | null;
96
+ coldBootReason?: ColdBootReason | null;
94
97
  /**
95
98
  * `result.durable` (0.12.0+), suspend and fork: `true` when the capture is in durable storage, `false` while its
96
99
  * durable copy is being written (see `durability`). `null` when the result does not say (other kinds, an older API).
@@ -105,6 +108,12 @@ export interface ServerTiming {
105
108
  * checkpoint before it (`restoredCheckpointId`, state as of `stateAsOf`). Null otherwise.
106
109
  */
107
110
  lostSuspend?: LostSuspend | null;
111
+ /**
112
+ * `result.host_lost` (0.13.1+): the machine the workspace ran on failed. A resume (or open) that restored the
113
+ * workspace says from what (`restoredFrom` `disk`, or `checkpoint` with `stateAsOf`); a suspend that found it so
114
+ * succeeds with only `detectedAt`. Null otherwise.
115
+ */
116
+ hostLost?: HostLost | null;
108
117
  /** `result.boot_to_ready_ms`: VM start until the guest agent answered. */
109
118
  bootToReadyMs: number | null;
110
119
  /** `result.host_timings_ms`: the host's own steps (restore: load, after_restore, ready, …). */
@@ -143,6 +152,33 @@ export interface LostSuspend {
143
152
  /** The restored state is as of this time (RFC 3339). */
144
153
  stateAsOf: string | null;
145
154
  }
155
+ /**
156
+ * `result.cold_boot_reason` (0.11.0+): `runtime_changed` (the platform's VM runtime changed after the suspend),
157
+ * `host_lost` (0.13.1+: the machine the workspace ran on failed) or `runtime_retired` (0.14.0+: the runtime the
158
+ * workspace was suspended on was retired after an announced window). Any other string is a reason this version does
159
+ * not know.
160
+ */
161
+ export type ColdBootReason = (typeof COLD_BOOT_REASONS)[number] | (string & {});
162
+ /** Every {@link ColdBootReason} this version knows (0.14.0+). */
163
+ export declare const COLD_BOOT_REASONS: readonly ["runtime_changed", "host_lost", "runtime_retired"];
164
+ /**
165
+ * `result.host_lost` (0.13.1+), camelCased: the machine the workspace ran on failed. The workspace was moved to
166
+ * `suspended` at that moment, and its next use (a resume, a tool call's wake, an open) restored it. See the lifecycle
167
+ * reference.
168
+ */
169
+ export interface HostLost {
170
+ /** When the failure was detected (RFC 3339). */
171
+ detectedAt: string | null;
172
+ /**
173
+ * What the resume restored: `disk` (the workspace's own disk, files kept; `coldBootReason` `host_lost`, processes
174
+ * restarted) or `checkpoint` (its newest checkpoint, see `stateAsOf`). Null on a suspend's result.
175
+ */
176
+ restoredFrom: 'disk' | 'checkpoint' | null;
177
+ /** `checkpoint`: the checkpoint the resume restored. */
178
+ restoredCheckpointId: string | null;
179
+ /** `checkpoint`: the restored state is as of this time (RFC 3339); changes after it are not in the workspace. */
180
+ stateAsOf: string | null;
181
+ }
146
182
  /**
147
183
  * The durable copy of a suspend or fork operation (0.12.0+): `result.durability` camelCased, or null when the result has
148
184
  * none (the suspend was stored durably before it completed, or another kind).
@@ -150,6 +186,11 @@ export interface LostSuspend {
150
186
  export declare function durabilityOf(op: Pick<Operation, 'result'>): Durability | null;
151
187
  /** A resume operation's `result.lost_suspend` camelCased (0.12.0+), or null. */
152
188
  export declare function lostSuspendOf(op: Pick<Operation, 'result'>): LostSuspend | null;
189
+ /**
190
+ * An operation's `result.host_lost` camelCased (0.13.1+), or null: on a resume or open that restored a workspace whose
191
+ * machine failed, and on a suspend that found it so (`detectedAt` only).
192
+ */
193
+ export declare function hostLostOf(op: Pick<Operation, 'result'>): HostLost | null;
153
194
  /**
154
195
  * Whether a suspend or fork operation's capture is in durable storage (0.12.0+): `result.durable`, else `true` for a
155
196
  * succeeded suspend or fork whose result predates the field, else null.
@@ -255,7 +296,10 @@ export declare function traced<T>(trace: Trace, fn: () => Promise<T>): Promise<T
255
296
  *
256
297
  * A call that retried a request adds `retries: <n> (<request>, <cause>, after <delay>)`.
257
298
  * A resume that booted the workspace instead of restoring its memory (0.11.0+) says so in the server line:
258
- * `resume from cold_boot: processes restarted (runtime_changed)`.
299
+ * `resume from cold_boot: processes restarted (runtime_changed)`, or `(host_lost)` (0.13.1+) when the machine the
300
+ * workspace ran on failed and it booted from its disk. A resume that restored the newest checkpoint after such a
301
+ * failure adds `restored checkpoint <id> (host_lost; state as of <time>)`; a suspend that found the machine failed
302
+ * adds `host_lost (detected <time>)`.
259
303
  */
260
304
  export declare function formatTiming(t: LifecycleTiming): string;
261
305
  export {};