@shardflux/sdk 0.11.0 → 0.12.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/client.d.ts CHANGED
@@ -22,15 +22,15 @@ import type { SaveAsTemplateParams, SaveAsTemplateResponse } from './templates.j
22
22
  import { UsageApi } from './usage.js';
23
23
  import { VolumesApi } from './volumes.js';
24
24
  import type { VersionCheckOption } from './version-check.js';
25
- import type { FinishedOperation, InternalLifecycleOptions, LifecycleOptions, ResumeOptions, WaitedLifecycleOptions, WaitedResumeOptions } from './lifecycle.js';
25
+ import type { FinishedOperation, InternalLifecycleOptions, LifecycleOptions, ResumeOptions, SuspendOptions, WaitedLifecycleOptions, WaitedResumeOptions, WaitedSuspendOptions } from './lifecycle.js';
26
26
  import type { ProgressListener } from './progress.js';
27
27
  import { CaptureRegistry } from './capture.js';
28
28
  import type { FeedbackReceipt, SendFeedbackParams } from './feedback.js';
29
29
  export type WorkspaceView = components['schemas']['Workspace'];
30
30
  export type Operation = components['schemas']['Operation'];
31
- /** persistent (kept until deleted) or session (discarded when the session ends: close(), idle timeout; contracts §19.11). */
31
+ /** persistent (kept until deleted) or session (discarded when the session ends: close(), idle timeout). */
32
32
  export type WorkspaceLifetime = components['schemas']['WorkspaceLifetime'];
33
- /** legacy (one disk) or layered (the template chain read-only plus a workspace layer; contracts §19.2). */
33
+ /** legacy (one disk) or layered (the template chain read-only plus a workspace layer). */
34
34
  export type DiskLayout = components['schemas']['DiskLayout'];
35
35
  /** standard, template_draft (a template's dev-mode draft) or template_test (a test instance of a draft state). */
36
36
  export type WorkspacePurpose = components['schemas']['WorkspacePurpose'];
@@ -56,7 +56,7 @@ type Ok<Op> = Op extends {
56
56
  [K in keyof R]: K extends 200 | 201 | 202 ? JsonOf<R[K]> : never;
57
57
  }[keyof R] : never;
58
58
  export type OpenResponse = Ok<operations['postV1WorkspacesOpen']>;
59
- /** POST /v1/workspaces/{id}/resume: the held 200 (contracts §22.6) or the lifecycle 202. */
59
+ /** POST /v1/workspaces/{id}/resume: the held 200 or the lifecycle 202. */
60
60
  export type ResumeResponse = Ok<operations['postV1WorkspacesWorkspaceIdResume']>;
61
61
  /** What a resume request answered (0.9.0; `WorkspacesApi.requestResume`). */
62
62
  export interface ResumeAnswer {
@@ -95,7 +95,7 @@ export type PortalSession = Ok<operations['postApiV1OrganizationsOrganizationIdB
95
95
  export type InvoicePage = Ok<operations['getApiV1OrganizationsOrganizationIdBillingInvoices']>;
96
96
  export type Invoice = InvoicePage['data'][number];
97
97
  /**
98
- * A pending suspend-when-idle request (0.10.0; contracts §20.6): once the workspace has been idle for `after_seconds`
98
+ * A pending suspend-when-idle request (0.10.0): once the workspace has been idle for `after_seconds`
99
99
  * (counted from the later of its last work and `requested_at`), it is suspended; `not_before` = requested_at +
100
100
  * after_seconds is the earliest.
101
101
  */
@@ -103,7 +103,7 @@ export type SuspendRequest = components['schemas']['SuspendRequest'];
103
103
  /** POST /v1/workspaces/{id}/suspend-when-idle (202). */
104
104
  export type SuspendWhenIdleResponse = Ok<operations['postV1WorkspacesWorkspaceIdSuspendWhenIdle']>;
105
105
  export interface SuspendWhenIdleOptions {
106
- /** Seconds the workspace must stay idle before it is suspended: an integer from 30 to 3600 (the API refuses others with 422 validation_failed). */
106
+ /** Seconds the workspace must stay idle before it is suspended: an integer from 0 (as soon as it is idle) to 3600 (the API refuses others with 422 validation_failed). */
107
107
  afterSeconds: number;
108
108
  /** Replays the stored response for a repeated request (default: a fresh key per call, so transport retries replay). */
109
109
  idempotencyKey?: string;
@@ -121,7 +121,7 @@ export interface ShardfluxOptions {
121
121
  apiKey: string;
122
122
  /** Default https://api.shardflux.dev (override with `baseUrl`). */
123
123
  baseUrl?: string;
124
- /** Default: the runtime's fetch, with `Connection: close` on Node 26 (undici 8 keep-alive stalls; see defaultFetch in http.ts). */
124
+ /** Default: pooled HTTP/1.1 on Node 26+, native fetch on other runtimes (see defaultFetch in http.ts). */
125
125
  fetch?: typeof fetch;
126
126
  userAgent?: string;
127
127
  /** Per-request timeout (ms), default 30 s. */
@@ -136,7 +136,7 @@ export interface ShardfluxOptions {
136
136
  */
137
137
  onProgress?: ProgressListener;
138
138
  /**
139
- * The automatic version check (0.9.0; contracts §30.4): after the first successful API response of the process, a
139
+ * The automatic version check (0.9.0): after the first successful API response of the process, a
140
140
  * background GET /v1/client-versions (3 s timeout, errors swallowed) emits a `ShardfluxUpdateWarning` when this
141
141
  * package is outdated or unsupported. Default true (`@shardflux/sdk` at SDK_VERSION); tools built on the SDK pass
142
142
  * their own `{ package, version }` or `false`. `SHARDFLUX_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1` turn it off.
@@ -155,16 +155,16 @@ export interface ForkTarget {
155
155
  }
156
156
  export interface WaitOptions {
157
157
  /**
158
- * Give up waiting after this long (default 300 000 ms); the operation continues server side. A start waiting for
159
- * capacity (`capacity_pending`) does so until its deadline (`error.details.deadline_at`, 15 minutes after it was
160
- * created) and then fails with `capacity_unavailable` (retryable; nothing was started).
158
+ * Give up waiting after this long (default 300 000 ms); the operation continues server side. A queued start
159
+ * (`capacity_pending`) has a deadline (`error.details.deadline_at`, 15 minutes after it was created); past it, it
160
+ * fails with `capacity_unavailable` (retryable; nothing was started).
161
161
  */
162
162
  timeoutMs?: number;
163
163
  /** First poll delay (default 250 ms); doubles up to `maxPollIntervalMs` with jitter. Used when the server does not wait. */
164
164
  pollIntervalMs?: number;
165
165
  maxPollIntervalMs?: number;
166
166
  /**
167
- * Ask the server to hold each poll until the state changes (`Prefer: wait`, contracts §3; default true). A server
167
+ * Ask the server to hold each poll until the state changes (`Prefer: wait`; default true). A server
168
168
  * that does not wait answers at once and the SDK falls back to the backoff above.
169
169
  */
170
170
  serverWait?: boolean;
@@ -172,7 +172,11 @@ export interface WaitOptions {
172
172
  /** Progress while waiting: each observed state (queued, capacity_pending, running with its reason), retries, and `done` with the timing. */
173
173
  onProgress?: ProgressListener;
174
174
  }
175
+ export type IdlePolicy = 'adaptive' | 'never' | `fixed:${number}`;
175
176
  export interface OpenParams {
177
+ /** Searchable metadata; supplied labels replace the existing map. */
178
+ labels?: Record<string, string>;
179
+ idlePolicy?: IdlePolicy;
176
180
  key: string;
177
181
  template: string;
178
182
  caps?: Caps;
@@ -189,7 +193,7 @@ export interface OpenParams {
189
193
  */
190
194
  secrets?: string[];
191
195
  /**
192
- * The template version's text inputs `{NAME: value}` (0.7.0; contracts §24.3), put into the environment of every
196
+ * The template version's text inputs `{NAME: value}` (0.7.0), put into the environment of every
193
197
  * exec, terminal, start command and service. A new key stores each given value, else the declared default; an
194
198
  * existing key replaces them all (omitted leaves them unchanged). Secret inputs are not passed here: they bind the
195
199
  * stored secret of the same name. ShardfluxApiError 422 with details.reason `input_unknown` (an undeclared name),
@@ -204,7 +208,7 @@ export interface OpenParams {
204
208
  */
205
209
  lifetime?: WorkspaceLifetime;
206
210
  /**
207
- * `file_first` (0.9.0; contracts §29): the workspace is a versioned file tree with no VM between executions. It is
211
+ * `file_first` (0.9.0): the workspace is a versioned file tree with no VM between executions. It is
208
212
  * ready at once (no operation: the open answers with a tool token), never suspended, and runs commands as executions
209
213
  * (`workspace.executions.run()`): a fresh VM on the latest tree whose changed files become the next tree revision;
210
214
  * nothing else survives an execution. Needs a layered template version (409 `layout_unsupported` otherwise) and is
@@ -225,6 +229,8 @@ export interface OpenParams {
225
229
  onProgress?: ProgressListener;
226
230
  }
227
231
  export interface ListParams {
232
+ /** All supplied labels must match exactly. */
233
+ labels?: Record<string, string>;
228
234
  state?: WorkspaceView['observed_state'];
229
235
  desiredState?: WorkspaceView['desired_state'];
230
236
  keyPrefix?: string;
@@ -248,7 +254,7 @@ export interface FindByKeyOptions {
248
254
  signal?: AbortSignal;
249
255
  }
250
256
  /**
251
- * The workspace a key names (contracts §19.11): the live row (deleted_at null) when there is one, since at most one live
257
+ * The workspace a key names: the live row (deleted_at null) when there is one, since at most one live
252
258
  * workspace holds a key; otherwise the newest tombstone (ended sessions leave tombstones with the same key, and a
253
259
  * deleted persistent key keeps its tombstone). Null when no row has exactly this key. Rows need not be sorted.
254
260
  */
@@ -283,15 +289,24 @@ export declare class WorkspacesApi {
283
289
  open(params: OpenParams): Promise<Workspace>;
284
290
  /**
285
291
  * Waits for an operation: each GET /v1/operations/{id} asks the server to hold the response until the state changes
286
- * (`Prefer: wait`, at most 20 s, contracts §3), so completion is seen within one notification of the commit. A server
292
+ * (`Prefer: wait`, at most 20 s), so completion is seen within one notification of the commit. A server
287
293
  * that does not wait is polled with bounded exponential backoff (+-20 % jitter). Resolves when the operation
288
294
  * succeeds; throws OperationFailedError when it fails or is canceled, OperationTimeoutError after `timeoutMs` (the
289
295
  * operation keeps running and can be awaited again). Both errors carry the wait's `timing`; `onProgress` sees each
290
- * state change (queued, capacity_pending, running, with the server's reason) as it is observed. A start waiting in
291
- * capacity_pending gives up at its deadline (`deadlineAt` on the phase event) and fails with `capacity_unavailable`
292
- * (`err.retryable` true: nothing was started, retry later); the SDK does not retry it.
296
+ * state change (queued, capacity_pending, running, with the server's reason) as it is observed. A start in
297
+ * capacity_pending that passes its deadline (`deadlineAt` on the phase event) fails with `capacity_unavailable`
298
+ * (`err.retryable` true: nothing was started; send it again); the SDK does not retry it.
293
299
  */
294
300
  waitForOperation(operationId: string, opts?: WaitOptions): Promise<Operation>;
301
+ /**
302
+ * Waits for the durable copy of a succeeded suspend or fork (0.12.0+): resolves with the operation once
303
+ * `result.durable` is true, typically within a second of the suspend. An operation already durable (or one whose result
304
+ * predates the field) resolves at once without a request. Polls GET /v1/operations/{id} (`pollIntervalMs`, default
305
+ * 250 ms, doubling up to `maxPollIntervalMs`, default 1 000 ms). Throws DurabilityLostError when the copy cannot be
306
+ * made (`durability.state` `lost`), OperationFailedError if the operation did not succeed, and OperationTimeoutError
307
+ * (`durable: true`) after `timeoutMs` (default 300 000 ms; the copy continues server side). `signal` aborts the wait.
308
+ */
309
+ waitForDurable(operation: string | Operation, opts?: WaitOptions): Promise<FinishedOperation>;
295
310
  /** One operation (GET /v1/operations/{id}); lifecycle operations stay pollable after a workspace is deleted. */
296
311
  getOperation(operationId: string, opts?: {
297
312
  signal?: AbortSignal;
@@ -300,6 +315,10 @@ export declare class WorkspacesApi {
300
315
  agentLabel?: string;
301
316
  tools?: ToolName[];
302
317
  }): Promise<Workspace>;
318
+ /** Replace labels. An empty map clears them. */
319
+ setLabels(workspaceId: string, labels: Record<string, string>): Promise<Workspace>;
320
+ /** null clears the override, restoring the template or platform policy. */
321
+ setIdlePolicy(workspaceId: string, idlePolicy: IdlePolicy | null): Promise<Workspace>;
303
322
  list(params?: ListParams): Promise<Page<Workspace>>;
304
323
  /** Iterates every page. */
305
324
  listAll(params?: Omit<ListParams, 'cursor'>): AsyncGenerator<Workspace>;
@@ -317,13 +336,15 @@ export declare class WorkspacesApi {
317
336
  delete(workspaceId: string, opts?: LifecycleOptions): Promise<Operation>;
318
337
  /**
319
338
  * Suspends the workspace (memory and processes checkpointed). Resolves when the suspend is REQUESTED: the returned
320
- * operation is usually still `queued`. Pass `{ wait: true }` to resolve once it has FINISHED (`succeeded`).
339
+ * operation is usually still `queued`. Pass `{ wait: true }` to resolve once it has FINISHED (`succeeded`): the
340
+ * workspace is sealed on its host, typically in a few hundred ms, and `result.durable` turns true when the copy lands
341
+ * in durable storage, typically within a second. `{ durable: true }` (0.12.0+) resolves only then (see SuspendOptions).
321
342
  */
322
- suspend(workspaceId: string, opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
323
- suspend(workspaceId: string, opts?: LifecycleOptions): Promise<Operation>;
343
+ suspend(workspaceId: string, opts: WaitedSuspendOptions): Promise<FinishedOperation>;
344
+ suspend(workspaceId: string, opts?: SuspendOptions): Promise<Operation>;
324
345
  /**
325
346
  * Resumes a suspended workspace. Resolves when the resume is requested; with `wait`, once the workspace runs. With
326
- * `wait` (0.9.0) the request is held by the server until the workspace runs (contracts §22.6: one request, timing phase
347
+ * `wait` (0.9.0) the request is held by the server until the workspace runs (one request, timing phase
327
348
  * `request` with reason `held`); a server that does not hold it answers at once and the operation is polled.
328
349
  * `serverWait: false` polls only. `agentLabel`/`tools` choose the tool token the held answer carries (attribution
329
350
  * only here; `workspace.resume()` keeps it). A workspace that is already running is ShardfluxApiError 409 `conflict`
@@ -333,7 +354,7 @@ export declare class WorkspacesApi {
333
354
  resume(workspaceId: string, opts?: ResumeOptions): Promise<Operation>;
334
355
  /**
335
356
  * One `POST /v1/workspaces/{id}/resume` (0.9.0), held for up to `waitS` seconds when that is at least 1 (`Prefer:
336
- * wait`, contracts §22.6), asking for the tool token of `agentLabel`/`tools`. `ready` only for a 200 the server says it
357
+ * wait`), asking for the tool token of `agentLabel`/`tools`. `ready` only for a 200 the server says it
337
358
  * held (`Preference-Applied`); every other answer is the operation to wait for, so a server without the held resume
338
359
  * works unchanged. Refusals (409 operation_in_progress, already_running without the hold, ...) throw as usual.
339
360
  * `Workspace.wake()` and `resume({ wait })` use it; most callers want those.
@@ -347,7 +368,7 @@ export declare class WorkspacesApi {
347
368
  label?: string;
348
369
  }): Promise<Operation>;
349
370
  /**
350
- * Suspends the workspace once it has been idle for `afterSeconds` (0.9.0; contracts §20.6): meant for the end of an
371
+ * Suspends the workspace once it has been idle for `afterSeconds` (0.9.0): meant for the end of an
351
372
  * agent turn, so the workspace stops using RAM soon after instead of waiting out its idle policy. The idle time
352
373
  * counts from the later of the workspace's last work and this request. A command still running, an attached exec or
353
374
  * terminal stream, or a keepalive postpones the suspend until `afterSeconds` after it ends; a tool call after the
@@ -357,7 +378,7 @@ export declare class WorkspacesApi {
357
378
  * Resolves with the recorded `suspendRequest`, or, when a suspend is already in progress, with that `operation` and
358
379
  * nothing recorded. Tool-call capture writes recorded before the call land first (a later write would count as the
359
380
  * next turn). Errors: ShardfluxApiError 409 with reason `not_running`, `operation_in_progress`, `session_lifetime` or
360
- * `workspace_deleted`; 422 `validation_failed` for `afterSeconds` outside 30..3600.
381
+ * `workspace_deleted`; 422 `validation_failed` for `afterSeconds` outside 0..3600.
361
382
  *
362
383
  * await cloud.workspaces.suspendWhenIdle(workspace.id, { afterSeconds: 60 });
363
384
  */
@@ -369,7 +390,7 @@ export declare class WorkspacesApi {
369
390
  */
370
391
  cancelSuspendWhenIdle(workspaceId: string): Promise<Workspace>;
371
392
  /**
372
- * Ends a session workspace now (contracts §19.11): the workspace is deleted exactly like delete() (ended_reason
393
+ * Ends a session workspace now: the workspace is deleted exactly like delete() (ended_reason
373
394
  * closed) and returns the `delete` operation (input.reason session_closed); the key then opens a NEW workspace.
374
395
  * Idempotent. A persistent workspace is ShardfluxApiError 409 (details.reason `not_session`): use
375
396
  * `workspace.close()`, which calls this only for sessions. With `wait`, resolves once the delete has finished.
@@ -382,7 +403,7 @@ export declare class WorkspacesApi {
382
403
  workspace: WorkspaceView;
383
404
  }>;
384
405
  /**
385
- * Resets a layered workspace to its template (contracts §19.12): every change in the workspace layer is wiped; key,
406
+ * Resets a layered workspace to its template: every change in the workspace layer is wiped; key,
386
407
  * id, template version, caps, secret bindings and volume attachments stay. Running: restarted on a blank layer
387
408
  * (processes are gone; old tool tokens get 409 stale_epoch and the SDK refreshes them). Suspended: stays suspended and
388
409
  * boots blank on the next resume. Returns the `reset` operation; its result names the recovery checkpoint (restorable
@@ -391,7 +412,7 @@ export declare class WorkspacesApi {
391
412
  reset(workspaceId: string, opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
392
413
  reset(workspaceId: string, opts?: LifecycleOptions): Promise<Operation>;
393
414
  /**
394
- * Saves a layered workspace as the next version of an organization template (contracts §19.8). A running workspace is
415
+ * Saves a layered workspace as the next version of an organization template. A running workspace is
395
416
  * captured briefly (`operation`, layer_snapshot); poll `build` with templates.builds.waitForBuild. Owners/admins and
396
417
  * API keys with a tool permission only.
397
418
  */
@@ -409,7 +430,7 @@ export declare class WorkspacesApi {
409
430
  operation: Operation;
410
431
  workspace: Workspace;
411
432
  }>;
412
- /** The workspace's text inputs `{NAME: value}` (0.7.0; contracts §24.3). Secret inputs are bound secrets, never listed. */
433
+ /** The workspace's text inputs `{NAME: value}` (0.7.0). Secret inputs are bound secrets, never listed. */
413
434
  inputs(workspaceId: string): Promise<Record<string, string>>;
414
435
  operations(workspaceId: string, params?: {
415
436
  limit?: number;
@@ -454,17 +475,17 @@ export declare class Shardflux {
454
475
  readonly egress: EgressPolicyApi;
455
476
  /** Organization audit trail (owner/admin; API keys are refused with 403). */
456
477
  readonly audit: AuditApi;
457
- /** Shared volumes: persistent storage attached to workspaces at a mount path (contracts §15). */
478
+ /** Shared volumes: persistent storage attached to workspaces at a mount path. */
458
479
  readonly volumes: VolumesApi;
459
480
  constructor(opts: ShardfluxOptions);
460
481
  /** The authenticated principal (the API key, its organization and project). */
461
482
  me(): Promise<Me>;
462
483
  entitlements(organizationId: string): Promise<Entitlements>;
463
484
  /**
464
- * Send feedback straight to the Shardflux founder (0.9.0+; POST /v1/feedback), who reads every message. Use it while
465
- * you work, the moment something fails unexpectedly, an error or doc is confusing, something is missing or slow, or a
466
- * workaround was needed; short and specific beats polished, and `context.requestId` / `errorCode` let the founder find
467
- * the logs. `context.client` defaults to `shardflux-sdk-ts/<SDK_VERSION>`. Returns `{ id, receivedAt, duplicate }`
485
+ * Send feedback straight to the Shardflux team (0.9.0+; POST /v1/feedback), who read every message. Use it while you
486
+ * work, the moment something fails unexpectedly, an error or doc is unclear, an option is missing, or a workaround was
487
+ * needed, and when your user asks for a capability, an option or a smoother workflow; short and specific beats
488
+ * polished, and `context.requestId` / `errorCode` let the team find the logs. `context.client` defaults to `shardflux-sdk-ts/<SDK_VERSION>`. Returns `{ id, receivedAt, duplicate }`
468
489
  * (`duplicate`: the same message from this key within 24 hours; no second email). 429 `rate_limited` (with
469
490
  * `retryAfterSeconds`) and 422 `validation_failed` are ShardfluxApiErrors; the call is never retried.
470
491
  */
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError } from "./errors.js";
1
+ import { DurabilityLostError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError } from "./errors.js";
2
2
  import { HttpClient, SDK_VERSION, SERVER_WAIT_MAX_S, defaultFetch, defaultSleep, pollWithWait, randomId } from "./http.js";
3
3
  import { Workspace } from "./workspace.js";
4
4
  import { AuditApi } from "./audit.js";
@@ -9,11 +9,11 @@ import { UsageApi } from "./usage.js";
9
9
  import { VolumesApi } from "./volumes.js";
10
10
  import { versionCheckHook } from "./version-check.js";
11
11
  import { AFTER_WAIT, HELD_RESUME, TRACE, runLifecycle, waitOptionsOf } from "./lifecycle.js";
12
- import { Trace, combineListeners, traced } from "./progress.js";
12
+ import { Trace, combineListeners, durabilityOf, isDurable, traced } from "./progress.js";
13
13
  import { CaptureRegistry } from "./capture.js";
14
14
  import { sendFeedback } from "./feedback.js";
15
15
  /**
16
- * The workspace a key names (contracts §19.11): the live row (deleted_at null) when there is one, since at most one live
16
+ * The workspace a key names: the live row (deleted_at null) when there is one, since at most one live
17
17
  * workspace holds a key; otherwise the newest tombstone (ended sessions leave tombstones with the same key, and a
18
18
  * deleted persistent key keeps its tombstone). Null when no row has exactly this key. Rows need not be sorted.
19
19
  */
@@ -61,7 +61,7 @@ function abortableSleep(sleep, ms, signal) {
61
61
  }
62
62
  export class WorkspacesApi {
63
63
  #ctx;
64
- /** The cell endpoint of the last tool token seen: an open pre-connects to it while the server works (contracts §22.3). */
64
+ /** The cell endpoint of the last tool token seen: an open pre-connects to it while the server works. */
65
65
  #cellHint = null;
66
66
  constructor(ctx) {
67
67
  this.#ctx = ctx;
@@ -96,6 +96,10 @@ export class WorkspacesApi {
96
96
  body.secrets = params.secrets;
97
97
  if (params.inputs !== undefined)
98
98
  body.inputs = params.inputs;
99
+ if (params.labels !== undefined)
100
+ body.labels = params.labels;
101
+ if (params.idlePolicy !== undefined)
102
+ body.idle_policy = params.idlePolicy;
99
103
  if (params.lifetime !== undefined)
100
104
  body.lifetime = params.lifetime;
101
105
  if (params.mode !== undefined)
@@ -108,7 +112,7 @@ export class WorkspacesApi {
108
112
  const started = Date.now();
109
113
  const timeoutMs = waitOpts?.timeoutMs ?? 300_000;
110
114
  if (waitOpts && waitOpts.serverWait !== false) {
111
- // Held open (contracts §22.3): the server answers once the operation is terminal (200 with the running workspace
115
+ // Held open: the server answers once the operation is terminal (200 with the running workspace
112
116
  // and a tool token) or the wait elapsed (202); a server without it answers 202 at once and the poll below runs.
113
117
  const s = Math.min(SERVER_WAIT_MAX_S, Math.floor(timeoutMs / 1000));
114
118
  if (s >= 1) {
@@ -188,13 +192,13 @@ export class WorkspacesApi {
188
192
  }
189
193
  /**
190
194
  * Waits for an operation: each GET /v1/operations/{id} asks the server to hold the response until the state changes
191
- * (`Prefer: wait`, at most 20 s, contracts §3), so completion is seen within one notification of the commit. A server
195
+ * (`Prefer: wait`, at most 20 s), so completion is seen within one notification of the commit. A server
192
196
  * that does not wait is polled with bounded exponential backoff (+-20 % jitter). Resolves when the operation
193
197
  * succeeds; throws OperationFailedError when it fails or is canceled, OperationTimeoutError after `timeoutMs` (the
194
198
  * operation keeps running and can be awaited again). Both errors carry the wait's `timing`; `onProgress` sees each
195
- * state change (queued, capacity_pending, running, with the server's reason) as it is observed. A start waiting in
196
- * capacity_pending gives up at its deadline (`deadlineAt` on the phase event) and fails with `capacity_unavailable`
197
- * (`err.retryable` true: nothing was started, retry later); the SDK does not retry it.
199
+ * state change (queued, capacity_pending, running, with the server's reason) as it is observed. A start in
200
+ * capacity_pending that passes its deadline (`deadlineAt` on the phase event) fails with `capacity_unavailable`
201
+ * (`err.retryable` true: nothing was started; send it again); the SDK does not retry it.
198
202
  */
199
203
  async waitForOperation(operationId, opts = {}) {
200
204
  const inherited = opts[TRACE];
@@ -246,6 +250,61 @@ export class WorkspacesApi {
246
250
  interval = Math.min(maxInterval, interval * 2);
247
251
  }
248
252
  }
253
+ /**
254
+ * Waits for the durable copy of a succeeded suspend or fork (0.12.0+): resolves with the operation once
255
+ * `result.durable` is true, typically within a second of the suspend. An operation already durable (or one whose result
256
+ * predates the field) resolves at once without a request. Polls GET /v1/operations/{id} (`pollIntervalMs`, default
257
+ * 250 ms, doubling up to `maxPollIntervalMs`, default 1 000 ms). Throws DurabilityLostError when the copy cannot be
258
+ * made (`durability.state` `lost`), OperationFailedError if the operation did not succeed, and OperationTimeoutError
259
+ * (`durable: true`) after `timeoutMs` (default 300 000 ms; the copy continues server side). `signal` aborts the wait.
260
+ */
261
+ async waitForDurable(operation, opts = {}) {
262
+ const inherited = opts[TRACE];
263
+ const id = typeof operation === 'string' ? operation : operation.id;
264
+ const trace = inherited ?? new Trace('wait', combineListeners(this.#ctx().onProgress, opts.onProgress), { operationId: id });
265
+ const run = async () => {
266
+ const { sleep, http } = this.#ctx();
267
+ const timeoutMs = opts.timeoutMs ?? 300_000;
268
+ const maxInterval = opts.maxPollIntervalMs ?? 1_000;
269
+ let interval = opts.pollIntervalMs ?? 250;
270
+ const started = Date.now();
271
+ const aborted = () => (opts.signal?.reason instanceof Error ? opts.signal.reason : new Error('aborted'));
272
+ let op = typeof operation === 'string' ? null : operation;
273
+ for (;;) {
274
+ if (opts.signal?.aborted)
275
+ throw aborted();
276
+ if (op === null) {
277
+ const { body } = await pollWithWait(http, `/v1/operations/${encodeURIComponent(id)}`, this.#ctx().authorization, 0, opts.signal, trace.onRetry);
278
+ op = body.operation;
279
+ trace.observe(op);
280
+ }
281
+ if (op.state !== 'succeeded') {
282
+ if (TERMINAL.has(op.state))
283
+ throw new OperationFailedError(op);
284
+ // Not finished yet (a fork or suspend passed by id): wait for it first, then for its copy.
285
+ op = await this.waitForOperation(id, { ...opts, timeoutMs: Math.max(1, timeoutMs - (Date.now() - started)), [TRACE]: trace });
286
+ }
287
+ if (isDurable(op) !== false)
288
+ return op;
289
+ const durability = durabilityOf(op);
290
+ if (durability?.state === 'lost')
291
+ throw new DurabilityLostError(op);
292
+ trace.phase('durable', durability?.overdueAt ? 'overdue' : null);
293
+ const waited = Date.now() - started;
294
+ if (waited >= timeoutMs)
295
+ throw new OperationTimeoutError(op, waited, true);
296
+ const jitter = interval * 0.2 * (Math.random() * 2 - 1);
297
+ const delay = Math.max(10, Math.min(interval + jitter, timeoutMs - waited));
298
+ await (opts.signal ? abortableSleep(sleep, delay, opts.signal) : sleep(delay));
299
+ interval = Math.min(maxInterval, interval * 2);
300
+ op = null;
301
+ }
302
+ };
303
+ if (inherited)
304
+ return run();
305
+ trace.phase('request');
306
+ return traced(trace, run);
307
+ }
249
308
  /** One operation (GET /v1/operations/{id}); lifecycle operations stay pollable after a workspace is deleted. */
250
309
  async getOperation(operationId, opts = {}) {
251
310
  const { operation } = await this.#http.json('GET', `/v1/operations/${encodeURIComponent(operationId)}`, opts.signal ? { signal: opts.signal } : {}, this.#auth);
@@ -257,12 +316,21 @@ export class WorkspacesApi {
257
316
  async get(workspaceId, opts = {}) {
258
317
  return this.#wrap(await this.#getView(workspaceId), opts);
259
318
  }
319
+ /** Replace labels. An empty map clears them. */
320
+ async setLabels(workspaceId, labels) {
321
+ return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/labels`, { json: { labels } }, this.#auth));
322
+ }
323
+ /** null clears the override, restoring the template or platform policy. */
324
+ async setIdlePolicy(workspaceId, idlePolicy) {
325
+ return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/idle-policy`, { json: { idle_policy: idlePolicy } }, this.#auth));
326
+ }
260
327
  async list(params = {}) {
261
328
  const page = await this.#http.json('GET', '/v1/workspaces', {
262
329
  query: {
263
330
  state: params.state,
264
331
  desired_state: params.desiredState,
265
332
  key_prefix: params.keyPrefix,
333
+ labels: params.labels === undefined ? undefined : JSON.stringify(params.labels),
266
334
  project_id: params.projectId,
267
335
  organization_id: params.organizationId,
268
336
  include_deleted: params.includeDeleted,
@@ -379,7 +447,7 @@ export class WorkspacesApi {
379
447
  }
380
448
  /**
381
449
  * One `POST /v1/workspaces/{id}/resume` (0.9.0), held for up to `waitS` seconds when that is at least 1 (`Prefer:
382
- * wait`, contracts §22.6), asking for the tool token of `agentLabel`/`tools`. `ready` only for a 200 the server says it
450
+ * wait`), asking for the tool token of `agentLabel`/`tools`. `ready` only for a 200 the server says it
383
451
  * held (`Preference-Applied`); every other answer is the operation to wait for, so a server without the held resume
384
452
  * works unchanged. Refusals (409 operation_in_progress, already_running without the hold, ...) throw as usual.
385
453
  * `Workspace.wake()` and `resume({ wait })` use it; most callers want those.
@@ -406,11 +474,11 @@ export class WorkspacesApi {
406
474
  const res = await this.#http.jsonWithHeaders('POST', `/v1/workspaces/${encodeURIComponent(workspaceId)}/resume`, init, this.#auth);
407
475
  const b = res.body;
408
476
  if (!b || typeof b !== 'object' || !b.workspace)
409
- throw new ShardfluxProtocolError('resume: response has no workspace', res.status);
477
+ throw new ShardfluxProtocolError('resume: response has no workspace', res.status, 'api');
410
478
  const pa = res.headers.get('preference-applied');
411
479
  const ready = held && res.status === 200 && pa !== null && /\bwait\s*=/i.test(pa);
412
480
  if (!ready && !b.operation)
413
- throw new ShardfluxProtocolError('resume: response has no operation', res.status);
481
+ throw new ShardfluxProtocolError('resume: response has no operation', res.status, 'api');
414
482
  const toolToken = ready ? (b.tool_token ?? null) : null;
415
483
  this.#noteToken(toolToken);
416
484
  return { ready, operation: b.operation ?? null, workspace: b.workspace, toolToken, requestId: res.headers.get('x-request-id') };
@@ -419,7 +487,7 @@ export class WorkspacesApi {
419
487
  return this.#op('snapshot', workspaceId, opts.label === undefined ? {} : { label: opts.label }, opts);
420
488
  }
421
489
  /**
422
- * Suspends the workspace once it has been idle for `afterSeconds` (0.9.0; contracts §20.6): meant for the end of an
490
+ * Suspends the workspace once it has been idle for `afterSeconds` (0.9.0): meant for the end of an
423
491
  * agent turn, so the workspace stops using RAM soon after instead of waiting out its idle policy. The idle time
424
492
  * counts from the later of the workspace's last work and this request. A command still running, an attached exec or
425
493
  * terminal stream, or a keepalive postpones the suspend until `afterSeconds` after it ends; a tool call after the
@@ -429,7 +497,7 @@ export class WorkspacesApi {
429
497
  * Resolves with the recorded `suspendRequest`, or, when a suspend is already in progress, with that `operation` and
430
498
  * nothing recorded. Tool-call capture writes recorded before the call land first (a later write would count as the
431
499
  * next turn). Errors: ShardfluxApiError 409 with reason `not_running`, `operation_in_progress`, `session_lifetime` or
432
- * `workspace_deleted`; 422 `validation_failed` for `afterSeconds` outside 30..3600.
500
+ * `workspace_deleted`; 422 `validation_failed` for `afterSeconds` outside 0..3600.
433
501
  *
434
502
  * await cloud.workspaces.suspendWhenIdle(workspace.id, { afterSeconds: 60 });
435
503
  */
@@ -465,7 +533,7 @@ export class WorkspacesApi {
465
533
  return this.#op('reset', workspaceId, body, opts);
466
534
  }
467
535
  /**
468
- * Saves a layered workspace as the next version of an organization template (contracts §19.8). A running workspace is
536
+ * Saves a layered workspace as the next version of an organization template. A running workspace is
469
537
  * captured briefly (`operation`, layer_snapshot); poll `build` with templates.builds.waitForBuild. Owners/admins and
470
538
  * API keys with a tool permission only.
471
539
  */
@@ -488,7 +556,7 @@ export class WorkspacesApi {
488
556
  }, { settle: true });
489
557
  return { operation, workspace: copy };
490
558
  }
491
- /** The workspace's text inputs `{NAME: value}` (0.7.0; contracts §24.3). Secret inputs are bound secrets, never listed. */
559
+ /** The workspace's text inputs `{NAME: value}` (0.7.0). Secret inputs are bound secrets, never listed. */
492
560
  async inputs(workspaceId) {
493
561
  const body = await this.#http.json('GET', `/v1/workspaces/${encodeURIComponent(workspaceId)}/inputs`, {}, this.#auth);
494
562
  return body.inputs;
@@ -546,7 +614,7 @@ export class Shardflux {
546
614
  egress;
547
615
  /** Organization audit trail (owner/admin; API keys are refused with 403). */
548
616
  audit;
549
- /** Shared volumes: persistent storage attached to workspaces at a mount path (contracts §15). */
617
+ /** Shared volumes: persistent storage attached to workspaces at a mount path. */
550
618
  volumes;
551
619
  #ctx;
552
620
  constructor(opts) {
@@ -583,10 +651,10 @@ export class Shardflux {
583
651
  return this.#ctx.http.json('GET', `/v1/organizations/${encodeURIComponent(organizationId)}/entitlements`, {}, this.#ctx.authorization);
584
652
  }
585
653
  /**
586
- * Send feedback straight to the Shardflux founder (0.9.0+; POST /v1/feedback), who reads every message. Use it while
587
- * you work, the moment something fails unexpectedly, an error or doc is confusing, something is missing or slow, or a
588
- * workaround was needed; short and specific beats polished, and `context.requestId` / `errorCode` let the founder find
589
- * the logs. `context.client` defaults to `shardflux-sdk-ts/<SDK_VERSION>`. Returns `{ id, receivedAt, duplicate }`
654
+ * Send feedback straight to the Shardflux team (0.9.0+; POST /v1/feedback), who read every message. Use it while you
655
+ * work, the moment something fails unexpectedly, an error or doc is unclear, an option is missing, or a workaround was
656
+ * needed, and when your user asks for a capability, an option or a smoother workflow; short and specific beats
657
+ * polished, and `context.requestId` / `errorCode` let the team find the logs. `context.client` defaults to `shardflux-sdk-ts/<SDK_VERSION>`. Returns `{ id, receivedAt, duplicate }`
590
658
  * (`duplicate`: the same message from this key within 24 hours; no second email). 429 `rate_limited` (with
591
659
  * `retryAfterSeconds`) and 422 `validation_failed` are ShardfluxApiErrors; the call is never retried.
592
660
  */
package/dist/egress.d.ts CHANGED
@@ -50,7 +50,7 @@ export interface EgressPolicyVersion {
50
50
  created_at: string;
51
51
  }
52
52
  export interface EffectiveEgressPolicy {
53
- /** organization: an organization egress override (contracts §18) wins over every policy (mode deny_all, no version). */
53
+ /** organization: an organization egress override wins over every policy (mode deny_all, no version). */
54
54
  source: 'organization' | 'workspace' | 'project' | 'platform_default';
55
55
  policy_version_id: string | null;
56
56
  policy_version: number | null;
@@ -60,7 +60,7 @@ export interface EffectiveEgressPolicy {
60
60
  policy_sha256: string;
61
61
  }
62
62
  /**
63
- * Active organization egress override (contracts §18): the organization used its whole outbound transfer allowance,
63
+ * Active organization egress override: the organization used its whole outbound transfer allowance,
64
64
  * so outbound internet traffic of every workspace is blocked until `lifts_at` (period end) or an upgrade/purchase.
65
65
  * Stored policies are kept and apply again when it lifts.
66
66
  */
@@ -123,7 +123,7 @@ export interface WorkspaceEgressPolicy {
123
123
  organization_override: OrganizationEgressOverride | null;
124
124
  enforcement: EgressEnforcement;
125
125
  /**
126
- * The template version's network ceiling (0.7.0; contracts §24.3 `defaults.egress`), or null. The host enforces
126
+ * The template version's network ceiling (0.7.0 `defaults.egress`), or null. The host enforces
127
127
  * `effective` intersected with it; a workspace policy it would narrow is refused with 422 egress_widening.
128
128
  */
129
129
  template_egress: {