@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.
package/dist/errors.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { components as AppComponents } from './generated/app-api.js';
2
2
  import type { components as CellComponents } from './generated/cell-api.js';
3
- import type { LifecycleTiming } from './progress.js';
3
+ import type { Durability, LifecycleTiming } from './progress.js';
4
4
  export type AppErrorBody = AppComponents['schemas']['ErrorBody'];
5
5
  export type AppErrorCode = AppErrorBody['error']['code'];
6
6
  export type CellErrorCode = CellComponents['schemas']['ErrorCode'];
@@ -11,7 +11,7 @@ export type ErrorCode = AppErrorCode | CellErrorCode;
11
11
  * added: 409 conflict legacy_disk_layout, not_session, session_lifetime, lifetime_mismatch,
12
12
  * not_resettable, template_not_layered, draft_exists, draft_stale, build_in_progress, file_list_unavailable,
13
13
  * file_list_indexing (retryable), guest_feature_unavailable; 422 validation_failed confirm_destructive_required,
14
- * reserved_key_prefix, invalid_defaults, update_policy_not_available, invalid_path, too_many_acknowledged_findings;
14
+ * reserved_key_prefix, invalid_defaults, invalid_path, too_many_acknowledged_findings;
15
15
  * 403 forbidden template_dev_mode_role; 404 not_found draft_not_found, version_not_found, path_not_found.
16
16
  * The template editor (0.7.0) added: 422 validation_failed invalid_recipe, base_not_layered,
17
17
  * language_unavailable, language_conflict, invalid_package, too_many_files, platform_owned_path, upload_required,
@@ -51,8 +51,27 @@ export type ErrorCode = AppErrorCode | CellErrorCode;
51
51
  * overage_unavailable, spend_cap_required, spend_cap_below_minimum (details.min_minor), spend_cap_above_plan_price
52
52
  * (details.max_minor) and spend_cap_below_charges (details.charges_minor); with `ifMatch`, 409 conflict
53
53
  * version_mismatch (details.current_version), as egress puts with `ifMatch` answer too.
54
+ * Elastic memory (0.13.0): 422 validation_failed allocation_mode_not_available (details.field
55
+ * `caps.allocation_mode`; the organization does not have elastic memory, nothing is created or changed),
56
+ * not_supported_for_mode (elastic with file_first), requires_elastic and exceeds_memory_mib (details.field
57
+ * `caps.memory_mib_held`).
58
+ * Burst execution (0.13.0): the codes `burst_unavailable` (409; details.reason
59
+ * not_available, layout_unsupported, shared_volumes, host_capacity, fence_not_drained, workspace_fenced, apply_pending,
60
+ * park_failed, workspace_resumed, interrupted; details.replayed on a journaled failure answered again) and
61
+ * `burst_apply_failed` (409; details.reason disk_full, apply_failed, reverted, revert_failed with
62
+ * details.applied_entries and details.pending_entries: retry with the same session id to finish an apply_failed one);
63
+ * 422 validation_failed burst_mode_not_supported (`auto`), burst_not_supported (with stdin; also 409 conflict for
64
+ * signal/cancel of a burst session) and burst_size_exceeds_plan (details.field, details.limit); a burst session nobody
65
+ * followed any more ends with burst.error service_unavailable burst_lost.
66
+ * Immutable paths (0.13.0): 422 validation_failed immutable_path_removed (a recipe drops a path of the template's open
67
+ * version: details.removed, details.open_version; a version keeps every immutable path), immutable_paths_unsupported_base
68
+ * (the base's guest agent lacks the feature, or a Dockerfile build of a template with immutable paths: details.base
69
+ * {name, version}, details.required_feature, details.paths) and invalid_path at details.field `recipe.immutable[<i>]`;
70
+ * 409 conflict read_only_path (a files write under an immutable path, from the cell gateway). The reason
71
+ * `update_policy_not_available` is gone with the update policy. A build that fails on them carries `failure.code`
72
+ * immutable_path_missing (details.path) or immutable_image_too_large.
54
73
  */
55
- export type KnownErrorReason = 'invalid_recipe' | 'base_not_layered' | 'language_unavailable' | 'language_conflict' | 'invalid_package' | 'too_many_files' | 'platform_owned_path' | 'upload_required' | 'upload_missing' | 'upload_digest_mismatch' | 'upload_too_large' | 'extra_hosts_without_auto' | 'invalid_settings' | 'services_unsupported' | 'input_required' | 'input_unknown' | 'input_invalid' | 'egress_widening' | 'reserved_session_id' | 'env_collision' | 'reserved_template_slug' | 'package_index_unavailable' | 'package_not_found' | 'startup_failed' | 'service_not_ready' | 'secrets_unavailable' | 'workspace_not_running' | 'operation_in_progress' | 'workspace_deleted' | 'secret_not_available' | 'legacy_disk_layout' | 'not_session' | 'session_lifetime' | 'lifetime_mismatch' | 'not_resettable' | 'template_not_layered' | 'draft_exists' | 'draft_stale' | 'build_in_progress' | 'file_list_unavailable' | 'file_list_indexing' | 'guest_feature_unavailable' | 'confirm_destructive_required' | 'reserved_key_prefix' | 'invalid_defaults' | 'update_policy_not_available' | 'invalid_path' | 'too_many_acknowledged_findings' | 'template_dev_mode_role' | 'draft_not_found' | 'version_not_found' | 'path_not_found' | 'revision_mismatch' | 'edit_not_found' | 'edit_ambiguous' | 'edit_not_text' | 'patch_invalid' | 'host_capacity' | 'wake_failed' | 'workspace_fenced' | 'offline_unavailable' | 'offline_budget' | 'offline_changed' | 'host_feature_unavailable' | 'not_supported_for_mode' | 'mode_mismatch' | 'mode_not_available' | 'layout_unsupported' | 'tree_revision_mismatch' | 'outside_tree_root' | 'execution_in_progress' | 'execution_id_reused' | 'operation_id_reused' | 'no_execution_host' | 'lease_expired' | 'host_unreachable' | 'host_restarted' | 'tree_moved' | 'blob_missing' | 'blob_corrupt' | 'exec_failed_to_start' | 'invalid_cwd' | 'allowance_used' | 'overage_paused' | 'spend_cap_reached' | 'overage_unavailable' | 'spend_cap_required' | 'spend_cap_below_minimum' | 'spend_cap_above_plan_price' | 'spend_cap_below_charges' | 'version_mismatch';
74
+ export type KnownErrorReason = 'invalid_recipe' | 'base_not_layered' | 'language_unavailable' | 'language_conflict' | 'invalid_package' | 'too_many_files' | 'platform_owned_path' | 'upload_required' | 'upload_missing' | 'upload_digest_mismatch' | 'upload_too_large' | 'extra_hosts_without_auto' | 'invalid_settings' | 'services_unsupported' | 'input_required' | 'input_unknown' | 'input_invalid' | 'egress_widening' | 'reserved_session_id' | 'env_collision' | 'reserved_template_slug' | 'package_index_unavailable' | 'package_not_found' | 'startup_failed' | 'service_not_ready' | 'secrets_unavailable' | 'workspace_not_running' | 'operation_in_progress' | 'workspace_deleted' | 'secret_not_available' | 'legacy_disk_layout' | 'not_session' | 'session_lifetime' | 'lifetime_mismatch' | 'not_resettable' | 'template_not_layered' | 'draft_exists' | 'draft_stale' | 'build_in_progress' | 'file_list_unavailable' | 'file_list_indexing' | 'guest_feature_unavailable' | 'confirm_destructive_required' | 'reserved_key_prefix' | 'invalid_defaults' | 'invalid_path' | 'too_many_acknowledged_findings' | 'template_dev_mode_role' | 'draft_not_found' | 'version_not_found' | 'path_not_found' | 'revision_mismatch' | 'edit_not_found' | 'edit_ambiguous' | 'edit_not_text' | 'patch_invalid' | 'host_capacity' | 'wake_failed' | 'workspace_fenced' | 'offline_unavailable' | 'offline_budget' | 'offline_changed' | 'host_feature_unavailable' | 'not_supported_for_mode' | 'mode_mismatch' | 'mode_not_available' | 'layout_unsupported' | 'tree_revision_mismatch' | 'outside_tree_root' | 'execution_in_progress' | 'execution_id_reused' | 'operation_id_reused' | 'no_execution_host' | 'lease_expired' | 'host_unreachable' | 'host_restarted' | 'tree_moved' | 'blob_missing' | 'blob_corrupt' | 'exec_failed_to_start' | 'invalid_cwd' | 'allowance_used' | 'overage_paused' | 'spend_cap_reached' | 'overage_unavailable' | 'spend_cap_required' | 'spend_cap_below_minimum' | 'spend_cap_above_plan_price' | 'spend_cap_below_charges' | 'version_mismatch' | 'allocation_mode_not_available' | 'requires_elastic' | 'exceeds_memory_mib' | 'burst_mode_not_supported' | 'burst_not_supported' | 'burst_size_exceeds_plan' | 'not_available' | 'shared_volumes' | 'fence_not_drained' | 'apply_pending' | 'park_failed' | 'workspace_resumed' | 'interrupted' | 'burst_lost' | 'disk_full' | 'apply_failed' | 'reverted' | 'revert_failed' | 'immutable_path_removed' | 'immutable_paths_unsupported_base' | 'read_only_path';
56
75
  /** A known reason, or any other string the server sends (reasons are open-ended). */
57
76
  export type ErrorReason = KnownErrorReason | (string & {});
58
77
  export interface ErrorBodyLike {
@@ -141,8 +160,12 @@ export declare function apiError(status: number, body: ErrorBodyLike, source: 'a
141
160
  /** The response was not the documented shape (e.g. a proxy error page). */
142
161
  export declare class ShardfluxProtocolError extends Error {
143
162
  readonly status: number;
144
- constructor(message: string, status: number);
163
+ /** The responding surface; unknown only for errors constructed by older callers. Never proof of deletion. */
164
+ readonly source: 'api' | 'cell' | 'unknown';
165
+ constructor(message: string, status: number, source?: 'api' | 'cell' | 'unknown');
145
166
  }
167
+ /** True only for an explicit API tombstone refusal. A 404, even a structured one, may be routing or scope. */
168
+ export declare function isWorkspaceGone(error: unknown): boolean;
146
169
  type Operation = AppComponents['schemas']['Operation'];
147
170
  /**
148
171
  * Waiting for an operation ran out of time. The operation keeps running server side:
@@ -163,13 +186,24 @@ export declare class OperationTimeoutError extends Error {
163
186
  readonly waitedMs: number;
164
187
  /** Where the time went: client phases, retries and the operation's own server timing so far. */
165
188
  timing: LifecycleTiming | undefined;
166
- constructor(op: Operation, waitedMs: number);
189
+ /**
190
+ * `durable` (0.12.0+): the wait was for the durable copy of a succeeded suspend or fork (`suspend({ durable: true })`,
191
+ * `waitForDurable()`); `lastState` is then `succeeded` and the copy continues server side.
192
+ */
193
+ readonly durable: boolean;
194
+ constructor(op: Operation, waitedMs: number, durable?: boolean);
167
195
  }
168
- /** The operation reached `failed` or `canceled`. */
196
+ /**
197
+ * The operation reached `failed` or `canceled`. A suspend-when-idle that found the workspace active (a tool call after
198
+ * the request, an attached stream) is `canceled` with `errorCode` `workspace_active` (`workspaceActive` true, 0.12.0+):
199
+ * nothing changed and the workspace keeps running. `waitUntilReady()` and `wake()` treat it as running, not as a failure.
200
+ */
169
201
  export declare class OperationFailedError extends Error {
170
202
  readonly operation: Operation;
171
203
  readonly operationId: string;
172
204
  readonly errorCode: string | null;
205
+ /** 0.12.0+: a suspend-when-idle canceled because the workspace was active; it keeps running, nothing changed. */
206
+ readonly workspaceActive: boolean;
173
207
  /**
174
208
  * `operation.error.retryable` (false when absent): the same request may succeed later. `capacity_unavailable` is
175
209
  * retryable: the start passed its deadline and nothing was started; send it again. The SDK never retries a failed
@@ -180,4 +214,18 @@ export declare class OperationFailedError extends Error {
180
214
  timing: LifecycleTiming | undefined;
181
215
  constructor(op: Operation);
182
216
  }
217
+ /**
218
+ * `suspend({ durable: true })` or `waitForDurable()` (0.12.0+): the suspend (or fork) succeeded, but its durable copy
219
+ * could not be made (`durability.state` `lost`). `durability` has the reason; see the lifecycle reference for what the
220
+ * next resume restores.
221
+ */
222
+ export declare class DurabilityLostError extends Error {
223
+ readonly operation: Operation;
224
+ readonly operationId: string;
225
+ readonly workspaceId: string | null;
226
+ readonly durability: Durability;
227
+ /** Where the time went. */
228
+ timing: LifecycleTiming | undefined;
229
+ constructor(op: Operation);
230
+ }
183
231
  export {};
package/dist/errors.js CHANGED
@@ -1,4 +1,4 @@
1
- import { capacityDeadlineOf } from "./progress.js";
1
+ import { capacityDeadlineOf, durabilityOf } from "./progress.js";
2
2
  export function isErrorBody(v) {
3
3
  if (typeof v !== 'object' || v === null || !('error' in v))
4
4
  return false;
@@ -129,12 +129,20 @@ export function apiError(status, body, source, retryAfterSeconds, treeRevision)
129
129
  /** The response was not the documented shape (e.g. a proxy error page). */
130
130
  export class ShardfluxProtocolError extends Error {
131
131
  status;
132
- constructor(message, status) {
132
+ /** The responding surface; unknown only for errors constructed by older callers. Never proof of deletion. */
133
+ source;
134
+ constructor(message, status, source = 'unknown') {
133
135
  super(message);
134
136
  this.name = 'ShardfluxProtocolError';
135
137
  this.status = status;
138
+ this.source = source;
136
139
  }
137
140
  }
141
+ /** True only for an explicit API tombstone refusal. A 404, even a structured one, may be routing or scope. */
142
+ export function isWorkspaceGone(error) {
143
+ return error instanceof ShardfluxApiError && error.source === 'api' &&
144
+ error.code === 'conflict' && error.reason === 'workspace_deleted';
145
+ }
138
146
  /**
139
147
  * Waiting for an operation ran out of time. The operation keeps running server side:
140
148
  * resume with `cloud.workspaces.waitForOperation(err.operationId)` or call open() again
@@ -154,10 +162,18 @@ export class OperationTimeoutError extends Error {
154
162
  waitedMs;
155
163
  /** Where the time went: client phases, retries and the operation's own server timing so far. */
156
164
  timing = undefined;
157
- constructor(op, waitedMs) {
165
+ /**
166
+ * `durable` (0.12.0+): the wait was for the durable copy of a succeeded suspend or fork (`suspend({ durable: true })`,
167
+ * `waitForDurable()`); `lastState` is then `succeeded` and the copy continues server side.
168
+ */
169
+ durable;
170
+ constructor(op, waitedMs, durable = false) {
158
171
  const deadlineAt = capacityDeadlineOf(op);
159
172
  const after = deadlineAt ? `it stays queued server side until ${deadlineAt} and fails with capacity_unavailable if it has not started by then.` : 'it continues server side.';
160
- super(`Operation ${op.id} (${op.kind}) is still ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''} after ${Math.round(waitedMs)} ms; ${after}`);
173
+ super(durable
174
+ ? `Operation ${op.id} (${op.kind}) succeeded; its durable copy was still ${durabilityOf(op)?.state ?? 'pending'} after ${Math.round(waitedMs)} ms and continues server side.`
175
+ : `Operation ${op.id} (${op.kind}) is still ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''} after ${Math.round(waitedMs)} ms; ${after}`);
176
+ this.durable = durable;
161
177
  this.name = 'OperationTimeoutError';
162
178
  this.operationId = op.id;
163
179
  this.workspaceId = op.workspace_id;
@@ -167,11 +183,17 @@ export class OperationTimeoutError extends Error {
167
183
  this.waitedMs = waitedMs;
168
184
  }
169
185
  }
170
- /** The operation reached `failed` or `canceled`. */
186
+ /**
187
+ * The operation reached `failed` or `canceled`. A suspend-when-idle that found the workspace active (a tool call after
188
+ * the request, an attached stream) is `canceled` with `errorCode` `workspace_active` (`workspaceActive` true, 0.12.0+):
189
+ * nothing changed and the workspace keeps running. `waitUntilReady()` and `wake()` treat it as running, not as a failure.
190
+ */
171
191
  export class OperationFailedError extends Error {
172
192
  operation;
173
193
  operationId;
174
194
  errorCode;
195
+ /** 0.12.0+: a suspend-when-idle canceled because the workspace was active; it keeps running, nothing changed. */
196
+ workspaceActive;
175
197
  /**
176
198
  * `operation.error.retryable` (false when absent): the same request may succeed later. `capacity_unavailable` is
177
199
  * retryable: the start passed its deadline and nothing was started; send it again. The SDK never retries a failed
@@ -187,6 +209,29 @@ export class OperationFailedError extends Error {
187
209
  this.operation = op;
188
210
  this.operationId = op.id;
189
211
  this.errorCode = code;
212
+ this.workspaceActive = code === 'workspace_active' && op.kind === 'suspend';
190
213
  this.retryable = op.error?.retryable === true;
191
214
  }
192
215
  }
216
+ /**
217
+ * `suspend({ durable: true })` or `waitForDurable()` (0.12.0+): the suspend (or fork) succeeded, but its durable copy
218
+ * could not be made (`durability.state` `lost`). `durability` has the reason; see the lifecycle reference for what the
219
+ * next resume restores.
220
+ */
221
+ export class DurabilityLostError extends Error {
222
+ operation;
223
+ operationId;
224
+ workspaceId;
225
+ durability;
226
+ /** Where the time went. */
227
+ timing = undefined;
228
+ constructor(op) {
229
+ const durability = durabilityOf(op) ?? { state: 'lost', checkpointId: null, generationId: null, localCommitAt: null, durableBy: null, durableAt: null, localCommitToDurableMs: null, overdueAt: null, reason: null };
230
+ super(`Operation ${op.id} (${op.kind}): the durable copy of checkpoint ${durability.checkpointId ?? '?'} was not made${durability.reason ? ` (${durability.reason})` : ''}`);
231
+ this.name = 'DurabilityLostError';
232
+ this.operation = op;
233
+ this.operationId = op.id;
234
+ this.workspaceId = op.workspace_id;
235
+ this.durability = durability;
236
+ }
237
+ }
package/dist/feedback.js CHANGED
@@ -33,7 +33,7 @@ function feedbackBody(params) {
33
33
  export async function sendFeedback(ctx, params) {
34
34
  const { status, body } = await ctx.http.jsonWithStatus('POST', '/v1/feedback', { json: feedbackBody(params) }, ctx.authorization);
35
35
  if (typeof body?.id !== 'string' || typeof body.received_at !== 'string' || typeof body.duplicate !== 'boolean') {
36
- throw new ShardfluxProtocolError('POST /v1/feedback: the response is not {id, received_at, duplicate}', status);
36
+ throw new ShardfluxProtocolError('POST /v1/feedback: the response is not {id, received_at, duplicate}', status, 'api');
37
37
  }
38
38
  return { id: body.id, receivedAt: body.received_at, duplicate: body.duplicate };
39
39
  }