@shardflux/sdk 0.5.0 → 0.6.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/cell.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ShardfluxApiError, ShardfluxProtocolError } from "./errors.js";
2
2
  import { HttpClient, defaultSleep, randomId } from "./http.js";
3
+ import { describeFailure, emitTo } from "./progress.js";
3
4
  /** Fills a path template that must exist in cell-api.yaml. */
4
5
  export function cellPath(template, params) {
5
6
  return template.replace(/\{([a-z_]+)\}/g, (_, name) => {
@@ -9,6 +10,9 @@ export function cellPath(template, params) {
9
10
  return encodeURIComponent(String(v));
10
11
  });
11
12
  }
13
+ export const DEFAULT_TRANSITION_TIMEOUT_MS = 120_000;
14
+ /** Wakes per call at most: a workspace that keeps being suspended again surfaces the refusal. */
15
+ const MAX_WAKES = 3;
12
16
  const b64 = (bytes) => Buffer.from(typeof bytes === 'string' ? Buffer.from(bytes, 'utf8') : bytes).toString('base64');
13
17
  const unb64 = (s) => (s ? new Uint8Array(Buffer.from(s, 'base64')) : new Uint8Array());
14
18
  /** Parses an NDJSON byte stream into objects (tolerates CRLF and a final unterminated line). */
@@ -68,7 +72,11 @@ export class CellClient {
68
72
  workspaceId;
69
73
  tokens;
70
74
  #opts;
75
+ #listener;
76
+ #wake;
77
+ #transitionTimeoutMs;
71
78
  #clients = new Map();
79
+ #closer = new AbortController();
72
80
  constructor(workspaceId, tokens, opts = {}) {
73
81
  this.workspaceId = workspaceId;
74
82
  this.tokens = tokens;
@@ -79,6 +87,9 @@ export class CellClient {
79
87
  maxRetries: opts.maxRetries ?? 2,
80
88
  sleep: opts.sleep ?? defaultSleep,
81
89
  };
90
+ this.#wake = opts.wake ?? null;
91
+ this.#transitionTimeoutMs = opts.transitionTimeoutMs ?? DEFAULT_TRANSITION_TIMEOUT_MS;
92
+ this.#listener = opts.onProgress;
82
93
  }
83
94
  #http(endpoint) {
84
95
  let c = this.#clients.get(endpoint);
@@ -88,18 +99,71 @@ export class CellClient {
88
99
  }
89
100
  return c;
90
101
  }
91
- /** One authorized request; refreshes the token once on stale_epoch / 401. */
102
+ /** True after close(): every request (and stream) of this client is aborted. */
103
+ get closed() {
104
+ return this.#closer.signal.aborted;
105
+ }
106
+ /**
107
+ * Aborts every in-flight and future request of this client, including exec output streams and PTY reads (commands
108
+ * keep running in the workspace: nothing is canceled there). Workspace.close() calls it.
109
+ */
110
+ close() {
111
+ if (!this.#closer.signal.aborted)
112
+ this.#closer.abort(new DOMException('The workspace handle was closed.', 'AbortError'));
113
+ }
114
+ /**
115
+ * One authorized request. Refreshes the token once on stale_epoch / 401. Lifecycle transitions (contracts §20.4),
116
+ * bounded in total by `transitionTimeoutMs`: a call refused with `workspace_busy` is retried after `Retry-After`; one
117
+ * refused with `workspace_not_running` (or whose token cannot be minted because the workspace is not running) wakes
118
+ * the workspace through `wake`, given the time left, and is retried with a fresh token, at most 3 wakes per call.
119
+ * Refused calls were never executed, so retrying is safe. When the budget is spent the refusal surfaces.
120
+ */
92
121
  async request(method, path, init = {}) {
93
- for (let attempt = 0;; attempt += 1) {
94
- const token = await this.tokens.get();
122
+ const closer = this.#closer.signal;
123
+ if (closer.aborted)
124
+ throw closer.reason;
125
+ const { wake: wakeAllowed = true, ...reqInit } = init;
126
+ const signal = reqInit.signal ? AbortSignal.any([reqInit.signal, closer]) : closer;
127
+ const deadline = Date.now() + this.#transitionTimeoutMs;
128
+ const t0 = performance.now();
129
+ const listener = this.#listener;
130
+ const emit = listener
131
+ ? (e) => emitTo([listener], { action: 'tool', workspaceId: this.workspaceId, operationId: null, atMs: Math.round((performance.now() - t0) * 10) / 10, ...e })
132
+ : undefined;
133
+ const retry = (cause, delayMs, attempt) => emit?.({ type: 'retry', retry: { atMs: Math.round((performance.now() - t0) * 10) / 10, request: `${method} ${path}`, attempt, cause, delayMs } });
134
+ let refreshed = false;
135
+ let wakes = 0;
136
+ let attempts = 0;
137
+ for (;;) {
95
138
  try {
96
- return await this.#http(token.cell_endpoint).raw(method, path, init, `Bearer ${token.token}`);
139
+ const token = await this.tokens.get(listener);
140
+ return await this.#http(token.cell_endpoint).raw(method, path, { ...reqInit, signal, ...(emit ? { onRetry: (r) => retry(r.cause, r.delayMs, (attempts += 1)) } : {}) }, `Bearer ${token.token}`);
97
141
  }
98
142
  catch (err) {
99
- const refreshable = err instanceof ShardfluxApiError && (err.code === 'stale_epoch' || err.status === 401);
100
- if (!refreshable || attempt > 0)
143
+ if (!(err instanceof ShardfluxApiError) || signal.aborted)
101
144
  throw err;
102
- this.tokens.invalidate();
145
+ if ((err.code === 'stale_epoch' || (err.status === 401 && err.source === 'cell')) && !refreshed) {
146
+ refreshed = true;
147
+ this.tokens.invalidate();
148
+ retry(err.code === 'stale_epoch' ? 'stale_epoch (the workspace moved or resumed; new token)' : `${describeFailure(err)} (new token)`, 0, (attempts += 1));
149
+ continue;
150
+ }
151
+ const left = deadline - Date.now();
152
+ if (err.code === 'workspace_busy' && left > 0) {
153
+ emit?.({ type: 'phase', phase: 'busy', reason: err.reason ?? 'workspace_busy' });
154
+ await this.#opts.sleep(Math.min(left, 5_000, Math.max(250, (err.retryAfterSeconds ?? 1) * 1000)));
155
+ continue;
156
+ }
157
+ const notRunning = err.code === 'workspace_not_running' || (err.code === 'conflict' && err.reason === 'workspace_not_running');
158
+ if (notRunning && wakeAllowed && this.#wake && wakes < MAX_WAKES && left > 0) {
159
+ wakes += 1;
160
+ if ((await this.#wake(left, signal)) === false)
161
+ throw err; // running per the API: nothing to wait for
162
+ this.tokens.invalidate();
163
+ refreshed = false;
164
+ continue;
165
+ }
166
+ throw err;
103
167
  }
104
168
  }
105
169
  }
@@ -130,6 +194,8 @@ export class CellClient {
130
194
  /** Output events from byte offsets (NDJSON). `follow` keeps the stream open until `exit`. */
131
195
  output: async (sessionId, opts = {}) => {
132
196
  const res = await this.request('GET', this.#p('/v1/workspaces/{workspace_id}/exec/{session_id}/output', { session_id: sessionId }), {
197
+ // Following never wakes: a workspace suspended under a running command stays suspended (explicit suspend).
198
+ wake: opts.follow === false,
133
199
  query: { stdout_offset: opts.stdoutOffset ?? 0, stderr_offset: opts.stderrOffset ?? 0, follow: opts.follow ?? true },
134
200
  accept: 'application/x-ndjson',
135
201
  timeoutMs: opts.follow === false ? undefined : 0,
@@ -223,7 +289,7 @@ export class CellClient {
223
289
  }
224
290
  }
225
291
  catch (e) {
226
- if (opts.signal?.aborted)
292
+ if (opts.signal?.aborted || this.closed)
227
293
  throw e;
228
294
  const transient = !(e instanceof ShardfluxApiError) || e.retryable;
229
295
  if (!transient || reconnects >= maxReconnects)
@@ -234,6 +300,8 @@ export class CellClient {
234
300
  break;
235
301
  }
236
302
  // Stream ended without `exit` (gateway restart, idle proxy, network): resume from offsets.
303
+ if (this.closed)
304
+ throw this.#closer.signal.reason;
237
305
  reconnects += 1;
238
306
  if (reconnects > maxReconnects)
239
307
  throw new ShardfluxProtocolError(`exec ${sessionId}: output stream kept dropping`, 0);
@@ -288,6 +356,7 @@ export class CellClient {
288
356
  let session = null;
289
357
  let exited = false;
290
358
  await new Promise((resolve, reject) => {
359
+ const onClose = () => finish(this.#closer.signal.reason instanceof Error ? this.#closer.signal.reason : new Error('closed'));
291
360
  const ws = new WS(url, { headers: { authorization: `Bearer ${token}` } });
292
361
  let quiet;
293
362
  const total = setTimeout(() => finish(), opts.timeoutMs ?? 5_000);
@@ -295,6 +364,7 @@ export class CellClient {
295
364
  clearTimeout(total);
296
365
  if (quiet)
297
366
  clearTimeout(quiet);
367
+ this.#closer.signal.removeEventListener('abort', onClose);
298
368
  try {
299
369
  ws.close(1000);
300
370
  }
@@ -333,6 +403,10 @@ export class CellClient {
333
403
  });
334
404
  ws.addEventListener('error', () => finish(new ShardfluxProtocolError('pty attach WebSocket failed', 0)));
335
405
  ws.addEventListener('close', () => finish());
406
+ if (this.closed)
407
+ onClose();
408
+ else
409
+ this.#closer.signal.addEventListener('abort', onClose, { once: true });
336
410
  });
337
411
  return { output: sink.text(), nextOffset: next, session, exited };
338
412
  },
@@ -394,6 +468,22 @@ export class CellClient {
394
468
  mkdir: (path, opts = {}) => this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/files/mkdir'), { json: { path, ...(opts.parents !== undefined ? { parents: opts.parents } : {}), ...(opts.mode !== undefined ? { mode: opts.mode } : {}) } }),
395
469
  move: (from, to, opts = {}) => this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/files/move'), { json: { from, to, ...(opts.overwrite !== undefined ? { overwrite: opts.overwrite } : {}) } }),
396
470
  };
471
+ // ---- changes against the template (layered workspaces, contracts §19.10) ------------------
472
+ /** One page of the workspace's changes against its template (needs the `files` tool). */
473
+ changes(params = {}) {
474
+ return this.#json('GET', this.#p('/v1/workspaces/{workspace_id}/changes'), {
475
+ query: { path_prefix: params.pathPrefix, limit: params.limit, cursor: params.cursor, hash: params.hash, summary: params.summary },
476
+ });
477
+ }
478
+ /** Every change under `pathPrefix`, following next_cursor. */
479
+ async *changesAll(params = {}) {
480
+ let cursor;
481
+ do {
482
+ const page = await this.changes({ ...params, ...(cursor === undefined ? {} : { cursor }) });
483
+ yield* page.data;
484
+ cursor = page.next_cursor ?? undefined;
485
+ } while (cursor !== undefined);
486
+ }
397
487
  // ---- git -------------------------------------------------------------------------------
398
488
  git = {
399
489
  clone: (req) => this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/git/clone'), { json: req, timeoutMs: (req.timeout_ms ?? 600_000) + 30_000 }),
package/dist/client.d.ts CHANGED
@@ -16,10 +16,28 @@ import { AuditApi } from './audit.js';
16
16
  import { EgressPolicyApi } from './egress.js';
17
17
  import { SecretsApi } from './secrets.js';
18
18
  import { TemplatesApi } from './templates.js';
19
+ import type { SaveAsTemplateParams, SaveAsTemplateResponse } from './templates.js';
19
20
  import { UsageApi } from './usage.js';
20
21
  import { VolumesApi } from './volumes.js';
22
+ import type { FinishedOperation, InternalLifecycleOptions, LifecycleOptions, WaitedLifecycleOptions } from './lifecycle.js';
23
+ import type { ProgressListener } from './progress.js';
21
24
  export type WorkspaceView = components['schemas']['Workspace'];
22
25
  export type Operation = components['schemas']['Operation'];
26
+ /** persistent (kept until deleted) or session (discarded when the session ends: close(), idle timeout; contracts §19.11). */
27
+ export type WorkspaceLifetime = components['schemas']['WorkspaceLifetime'];
28
+ /** legacy (one disk) or layered (the template chain read-only plus a workspace layer; contracts §19.2). */
29
+ export type DiskLayout = components['schemas']['DiskLayout'];
30
+ /** standard, template_draft (a template's dev-mode draft) or template_test (a test instance of a draft state). */
31
+ export type WorkspacePurpose = components['schemas']['WorkspacePurpose'];
32
+ /** Reserved (T2): always `pinned` in T1. */
33
+ export type UpdatePolicy = components['schemas']['UpdatePolicy'];
34
+ /** Where the workspace's disk came from: null, a fork, or a draft state (test instances). */
35
+ export type WorkspaceOrigin = components['schemas']['WorkspaceOrigin'];
36
+ export type ResetWorkspaceBody = components['schemas']['ResetWorkspaceBody'];
37
+ /** List filter: a lifetime or `any` (the list default is `persistent`). */
38
+ export type LifetimeFilter = WorkspaceLifetime | 'any';
39
+ /** List filter: a purpose or `any` (the list default is `standard`). */
40
+ export type PurposeFilter = WorkspacePurpose | 'any';
23
41
  type JsonOf<R> = R extends {
24
42
  content: {
25
43
  'application/json': infer T;
@@ -48,6 +66,7 @@ export interface ShardfluxOptions {
48
66
  apiKey: string;
49
67
  /** Default https://api.shardflux.dev (override with `baseUrl`). */
50
68
  baseUrl?: string;
69
+ /** Default: the runtime's fetch, with `Connection: close` on Node 26 (undici 8 keep-alive stalls; see defaultFetch in http.ts). */
51
70
  fetch?: typeof fetch;
52
71
  userAgent?: string;
53
72
  /** Per-request timeout (ms), default 30 s. */
@@ -56,19 +75,36 @@ export interface ShardfluxOptions {
56
75
  maxRetries?: number;
57
76
  /** Injected for tests; defaults to setTimeout. */
58
77
  sleep?: (ms: number) => Promise<void>;
78
+ /**
79
+ * Progress of every traced call made through this client (open, lifecycle calls, waits, wakes, tool tokens, and
80
+ * retries and busy waits of tool calls): for logs or telemetry. Per-call `onProgress` listeners get their own events too.
81
+ */
82
+ onProgress?: ProgressListener;
59
83
  }
60
84
  export interface Caps {
61
85
  cpu_millis?: number;
62
86
  memory_mib?: number;
63
87
  disk_gib?: number;
64
88
  }
89
+ export interface ForkTarget {
90
+ key: string;
91
+ caps?: Caps;
92
+ lifetime?: WorkspaceLifetime;
93
+ }
65
94
  export interface WaitOptions {
66
95
  /** Give up waiting after this long (default 300 000 ms); the operation continues server side. */
67
96
  timeoutMs?: number;
68
- /** First poll delay (default 250 ms); doubles up to `maxPollIntervalMs` with jitter. */
97
+ /** First poll delay (default 250 ms); doubles up to `maxPollIntervalMs` with jitter. Used when the server does not wait. */
69
98
  pollIntervalMs?: number;
70
99
  maxPollIntervalMs?: number;
100
+ /**
101
+ * Ask the server to hold each poll until the state changes (`Prefer: wait`, contracts §3; default true). A server
102
+ * that does not wait answers at once and the SDK falls back to the backoff above.
103
+ */
104
+ serverWait?: boolean;
71
105
  signal?: AbortSignal;
106
+ /** Progress while waiting: each observed state (queued, capacity_pending, running with its reason), retries, and `done` with the timing. */
107
+ onProgress?: ProgressListener;
72
108
  }
73
109
  export interface OpenParams {
74
110
  key: string;
@@ -80,10 +116,28 @@ export interface OpenParams {
80
116
  agentLabel?: string;
81
117
  /** Tools to request in tool tokens (subset of the key's permissions); default all permitted. */
82
118
  tools?: ToolName[];
119
+ /**
120
+ * Secret names to bind to the workspace (max 50): injected into every exec and PTY start. Sets the binding of a
121
+ * new key and replaces it on an existing key; omitted leaves it unchanged. An unknown or unusable name is
122
+ * ShardfluxApiError 422 (details.reason `secret_not_available`, details.names) and nothing is created or changed.
123
+ */
124
+ secrets?: string[];
125
+ /**
126
+ * `session`: the workspace is discarded when its session ends (workspace.close(), or the idle timeout); the key then
127
+ * opens a NEW workspace. Omitted: the template version's default, else `persistent`. Immutable: reopening a live key
128
+ * with another value is ShardfluxApiError 409 (details.reason `lifetime_mismatch`).
129
+ */
130
+ lifetime?: WorkspaceLifetime;
83
131
  /** `false`: return immediately (possibly not ready). Default: wait until ready. */
84
132
  wait?: false | WaitOptions;
85
133
  /** Defaults to a fresh key per open() call so transport retries replay instead of duplicating. */
86
134
  idempotencyKey?: string;
135
+ /**
136
+ * Progress of the open: the request, each operation state observed while waiting (with the server's reason), the
137
+ * view read and first tool token, and retries. The final `done` event carries the timing, also on
138
+ * `workspace.lastTiming` (and on the error's `timing` when the open fails).
139
+ */
140
+ onProgress?: ProgressListener;
87
141
  }
88
142
  export interface ListParams {
89
143
  state?: WorkspaceView['observed_state'];
@@ -92,9 +146,28 @@ export interface ListParams {
92
146
  projectId?: string;
93
147
  organizationId?: string;
94
148
  includeDeleted?: boolean;
149
+ /** Default (server side) `persistent`: sessions are hidden unless `session` or `any`. */
150
+ lifetime?: LifetimeFilter;
151
+ /** Default (server side) `standard`: drafts and test instances are hidden unless named or `any`. */
152
+ purpose?: PurposeFilter;
95
153
  limit?: number;
96
154
  cursor?: string;
97
155
  }
156
+ export interface FindByKeyOptions {
157
+ /** Also consider tombstones (deleted workspaces and ended sessions) when no live workspace has the key (default true). */
158
+ includeDeleted?: boolean;
159
+ projectId?: string;
160
+ organizationId?: string;
161
+ agentLabel?: string;
162
+ tools?: ToolName[];
163
+ signal?: AbortSignal;
164
+ }
165
+ /**
166
+ * The workspace a key names (contracts §19.11): the live row (deleted_at null) when there is one, since at most one live
167
+ * workspace holds a key; otherwise the newest tombstone (ended sessions leave tombstones with the same key, and a
168
+ * deleted persistent key keeps its tombstone). Null when no row has exactly this key. Rows need not be sorted.
169
+ */
170
+ export declare function pickByKey<T extends Pick<WorkspaceView, 'id' | 'workspace_key' | 'deleted_at'>>(rows: Iterable<T>, key: string): T | null;
98
171
  export interface Page<T> {
99
172
  data: T[];
100
173
  nextCursor: string | null;
@@ -107,6 +180,8 @@ export interface ClientContext {
107
180
  userAgent: string;
108
181
  sleep: (ms: number) => Promise<void>;
109
182
  workspaces: WorkspacesApi;
183
+ /** The client-level progress listener (ShardfluxOptions.onProgress). */
184
+ onProgress?: ProgressListener | undefined;
110
185
  }
111
186
  export declare class WorkspacesApi {
112
187
  #private;
@@ -115,12 +190,17 @@ export declare class WorkspacesApi {
115
190
  * Opens a workspace by key: creates it from the template's latest published version on first
116
191
  * use, reconnects (or resumes) afterwards; never resets an existing workspace. Waits until it is
117
192
  * ready unless `wait: false`; on timeout throws OperationTimeoutError carrying the operation id.
193
+ * The timing of the open (client phases, retries, the operation's server timing) is on `workspace.lastTiming`, on
194
+ * `onProgress` as it happens, and on the error's `timing` when the open fails.
118
195
  */
119
196
  open(params: OpenParams): Promise<Workspace>;
120
197
  /**
121
- * Polls GET /v1/operations/{id} with bounded exponential backoff (+-20 % jitter) until it
122
- * succeeds (resolves), fails or is canceled (OperationFailedError), or `timeoutMs` passes
123
- * (OperationTimeoutError; the operation keeps running and can be awaited again).
198
+ * Waits for an operation: each GET /v1/operations/{id} asks the server to hold the response until the state changes
199
+ * (`Prefer: wait`, at most 20 s, contracts §3), so completion is seen within one notification of the commit. A server
200
+ * that does not wait is polled with bounded exponential backoff (+-20 % jitter). Resolves when the operation
201
+ * succeeds; throws OperationFailedError when it fails or is canceled, OperationTimeoutError after `timeoutMs` (the
202
+ * operation keeps running and can be awaited again). Both errors carry the wait's `timing`; `onProgress` sees each
203
+ * state change (queued, capacity_pending, running, with the server's reason) as it is observed.
124
204
  */
125
205
  waitForOperation(operationId: string, opts?: WaitOptions): Promise<Operation>;
126
206
  /** One operation (GET /v1/operations/{id}); lifecycle operations stay pollable after a workspace is deleted. */
@@ -134,26 +214,72 @@ export declare class WorkspacesApi {
134
214
  list(params?: ListParams): Promise<Page<Workspace>>;
135
215
  /** Iterates every page. */
136
216
  listAll(params?: Omit<ListParams, 'cursor'>): AsyncGenerator<Workspace>;
137
- /** Tombstones the workspace now (tool access revoked); storage cleanup happens asynchronously. */
138
- delete(workspaceId: string, opts?: {
139
- idempotencyKey?: string;
140
- }): Promise<Operation>;
141
- suspend(workspaceId: string, opts?: {
142
- idempotencyKey?: string;
143
- }): Promise<Operation>;
144
- resume(workspaceId: string, opts?: {
145
- idempotencyKey?: string;
146
- }): Promise<Operation>;
147
- snapshot(workspaceId: string, opts?: {
217
+ /**
218
+ * Looks a workspace up by its exact key across every lifetime and purpose (`lifetime=any&purpose=any`), preferring the
219
+ * live workspace over tombstones of ended sessions or deleted workspaces with the same key (see pickByKey). Null when
220
+ * no workspace of the project has the key. This is the lookup the CLI and the MCP server use.
221
+ */
222
+ findByKey(key: string, opts?: FindByKeyOptions): Promise<Workspace | null>;
223
+ /**
224
+ * Deletes the workspace: tombstoned at once (tool access revoked), storage cleaned up by the operation. Resolves when
225
+ * the delete is requested; with `wait`, when it has finished.
226
+ */
227
+ delete(workspaceId: string, opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
228
+ delete(workspaceId: string, opts?: LifecycleOptions): Promise<Operation>;
229
+ /**
230
+ * Suspends the workspace (memory and processes checkpointed). Resolves when the suspend is REQUESTED: the returned
231
+ * operation is usually still `queued`. Pass `{ wait: true }` to resolve once it has FINISHED (`succeeded`).
232
+ */
233
+ suspend(workspaceId: string, opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
234
+ suspend(workspaceId: string, opts?: LifecycleOptions): Promise<Operation>;
235
+ /** Resumes a suspended workspace. Resolves when the resume is requested; with `wait`, once the workspace runs. */
236
+ resume(workspaceId: string, opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
237
+ resume(workspaceId: string, opts?: LifecycleOptions): Promise<Operation>;
238
+ /** Takes a snapshot. Resolves when it is requested; with `wait`, once it is taken. */
239
+ snapshot(workspaceId: string, opts: WaitedLifecycleOptions & {
240
+ label?: string;
241
+ }): Promise<FinishedOperation>;
242
+ snapshot(workspaceId: string, opts?: LifecycleOptions & {
148
243
  label?: string;
149
- idempotencyKey?: string;
150
244
  }): Promise<Operation>;
151
- fork(workspaceId: string, target: {
152
- key: string;
153
- caps?: Caps;
154
- }, opts?: {
155
- idempotencyKey?: string;
156
- }): Promise<{
245
+ /**
246
+ * Ends a session workspace now (contracts §19.11): the workspace is deleted exactly like delete() (ended_reason
247
+ * closed) and returns the `delete` operation (input.reason session_closed); the key then opens a NEW workspace.
248
+ * Idempotent. A persistent workspace is ShardfluxApiError 409 (details.reason `not_session`): use
249
+ * `workspace.close()`, which calls this only for sessions. With `wait`, resolves once the delete has finished.
250
+ */
251
+ close(workspaceId: string, opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
252
+ close(workspaceId: string, opts?: LifecycleOptions): Promise<Operation>;
253
+ /** close() plus the workspace view after the close (a tombstone). */
254
+ closeWithView(workspaceId: string, opts?: InternalLifecycleOptions): Promise<{
255
+ operation: Operation;
256
+ workspace: WorkspaceView;
257
+ }>;
258
+ /**
259
+ * Resets a layered workspace to its template (contracts §19.12): every change in the workspace layer is wiped; key,
260
+ * id, template version, caps, secret bindings and volume attachments stay. Running: restarted on a blank layer
261
+ * (processes are gone; old tool tokens get 409 stale_epoch and the SDK refreshes them). Suspended: stays suspended and
262
+ * boots blank on the next resume. Returns the `reset` operation; its result names the recovery checkpoint (restorable
263
+ * for 7 days). Errors: 409 legacy_disk_layout, not_resettable, operation_in_progress.
264
+ */
265
+ reset(workspaceId: string, opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
266
+ reset(workspaceId: string, opts?: LifecycleOptions): Promise<Operation>;
267
+ /**
268
+ * Saves a layered workspace as the next version of an organization template (contracts §19.8). A running workspace is
269
+ * captured briefly (`operation`, layer_snapshot); poll `build` with templates.builds.waitForBuild. Owners/admins and
270
+ * API keys with a tool permission only.
271
+ */
272
+ saveAsTemplate(workspaceId: string, params: SaveAsTemplateParams): Promise<SaveAsTemplateResponse>;
273
+ /**
274
+ * Forks into a new key. `lifetime` is the fork's own (default persistent): forking a session is how it is kept.
275
+ * Resolves when the fork is requested (the copy's handle is returned at once); with `wait`, once the copy exists,
276
+ * with its handle refreshed.
277
+ */
278
+ fork(workspaceId: string, target: ForkTarget, opts: WaitedLifecycleOptions): Promise<{
279
+ operation: FinishedOperation;
280
+ workspace: Workspace;
281
+ }>;
282
+ fork(workspaceId: string, target: ForkTarget, opts?: LifecycleOptions): Promise<{
157
283
  operation: Operation;
158
284
  workspace: Workspace;
159
285
  }>;