@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/CHANGELOG.md +68 -27
- package/README.md +123 -61
- package/dist/account.d.ts +3 -3
- package/dist/account.js +2 -2
- package/dist/cell.d.ts +36 -21
- package/dist/cell.js +49 -20
- package/dist/client.d.ts +56 -35
- package/dist/client.js +89 -21
- package/dist/egress.d.ts +3 -3
- package/dist/errors.d.ts +51 -22
- package/dist/errors.js +61 -16
- package/dist/executions.d.ts +2 -2
- package/dist/feedback.d.ts +3 -3
- package/dist/feedback.js +1 -1
- package/dist/generated/app-api.d.ts +1579 -30
- package/dist/generated/cell-api.d.ts +66 -0
- package/dist/http.d.ts +7 -11
- package/dist/http.js +32 -21
- package/dist/index.d.ts +10 -5
- package/dist/index.js +4 -2
- package/dist/lifecycle.d.ts +19 -2
- package/dist/lifecycle.js +9 -2
- package/dist/progress.d.ts +83 -22
- package/dist/progress.js +71 -10
- package/dist/tar.d.ts +1 -1
- package/dist/tar.js +1 -1
- package/dist/template-file.d.ts +1 -1
- package/dist/template-file.js +1 -1
- package/dist/templates.d.ts +21 -21
- package/dist/templates.js +8 -8
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +4 -4
- package/dist/version-check.d.ts +1 -1
- package/dist/volumes.d.ts +1 -1
- package/dist/workspace.d.ts +42 -27
- package/dist/workspace.js +45 -22
- package/package.json +4 -1
package/dist/cell.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (token expired or revoked early) invalidates the token and retries once with
|
|
8
8
|
* a fresh one; the retried request is safe because the gateway rejected the
|
|
9
9
|
* first before any effect (and exec/PTY starts are idempotent by session_id).
|
|
10
|
-
* The same holds for lifecycle refusals
|
|
10
|
+
* The same holds for lifecycle refusals: `workspace_busy` is
|
|
11
11
|
* waited out and `workspace_not_running` wakes the workspace, then the call is
|
|
12
12
|
* retried, all within one bounded transition budget per call.
|
|
13
13
|
*
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `exec.run()` survives gateway restarts/disconnects by reconnecting with the
|
|
16
16
|
* offsets it already processed; it never starts the command a second time.
|
|
17
17
|
*
|
|
18
|
-
* File-first workspaces
|
|
18
|
+
* File-first workspaces serve the files routes from a
|
|
19
19
|
* versioned tree and run commands as executions (`executions.run()`); the
|
|
20
20
|
* client tracks the tree revision every response reports (`X-Tree-Revision`)
|
|
21
21
|
* and, when it knows the workspace's mode, refuses calls the mode does not
|
|
@@ -31,6 +31,7 @@ import type { ToolTokenManager } from './tokens.js';
|
|
|
31
31
|
type S = components['schemas'];
|
|
32
32
|
export type ExecStartRequest = S['ExecStartRequest'];
|
|
33
33
|
export type ExecSession = S['ExecSession'];
|
|
34
|
+
export type ExecInputResult = S['ExecInputResult'];
|
|
34
35
|
export type OutputEvent = S['OutputEvent'];
|
|
35
36
|
export type PtyOpenRequest = S['PtyOpenRequest'];
|
|
36
37
|
export type PtySession = S['PtySession'];
|
|
@@ -39,20 +40,22 @@ export type ProcessList = S['ProcessList'];
|
|
|
39
40
|
export type FileInfo = S['FileInfo'];
|
|
40
41
|
export type FileList = S['FileList'];
|
|
41
42
|
export type FileWriteResult = S['FileWriteResult'];
|
|
42
|
-
/** Search request/result of `POST /files/search
|
|
43
|
+
/** Search request/result of `POST /files/search`. */
|
|
43
44
|
export type FileSearchRequest = S['FileSearchRequest'];
|
|
44
45
|
export type FileSearchMatch = S['FileSearchMatch'];
|
|
45
46
|
export type FileSearchResult = S['FileSearchResult'];
|
|
46
|
-
/** One text edit of `POST /files/patch
|
|
47
|
+
/** One text edit of `POST /files/patch`, as sent on the wire. */
|
|
47
48
|
export type FileEdit = S['FileEdit'];
|
|
48
49
|
export type FilePatchRequest = S['FilePatchRequest'];
|
|
49
50
|
export type FilePatchResult = S['FilePatchResult'];
|
|
50
51
|
/** A content SHA-256 (lowercase hex), or `absent` for a path that must not exist. */
|
|
51
52
|
export type FileRevision = S['FileRevision'];
|
|
52
53
|
export type WakeHintResult = S['WakeHintResult'];
|
|
53
|
-
|
|
54
|
+
export type IdleStatus = S['IdleStatus'];
|
|
55
|
+
export type KeepaliveResult = S['KeepaliveResult'];
|
|
56
|
+
/** Where a running workspace's VM is: resident, frozen, hibernated or restoring. */
|
|
54
57
|
export type Residency = WakeHintResult['residency'];
|
|
55
|
-
/** `X-Served-From`: `disk` when a read was served from a suspended or hibernated workspace's disk
|
|
58
|
+
/** `X-Served-From`: `disk` when a read was served from a suspended or hibernated workspace's disk. */
|
|
56
59
|
export type ServedFrom = 'guest' | 'disk';
|
|
57
60
|
/** `files.readWithInfo()`: the bytes plus what the gateway reported about the file. */
|
|
58
61
|
export interface FileReadResult {
|
|
@@ -122,7 +125,7 @@ export type BrowserScreenshotRequest = S['BrowserScreenshotRequest'];
|
|
|
122
125
|
export type BrowserContentRequest = S['BrowserContentRequest'];
|
|
123
126
|
export type BrowserContent = S['BrowserContent'];
|
|
124
127
|
export type Signal = S['SignalValue'];
|
|
125
|
-
/** One entry of a workspace's changes against its template
|
|
128
|
+
/** One entry of a workspace's changes against its template. */
|
|
126
129
|
export type WorkspaceChange = S['WorkspaceChange'];
|
|
127
130
|
export type WorkspaceChangeKind = WorkspaceChange['change'];
|
|
128
131
|
export type WorkspaceChangesSummary = S['WorkspaceChangesSummary'];
|
|
@@ -155,8 +158,8 @@ export interface CellClientOptions {
|
|
|
155
158
|
* resolves `false` when there was nothing to wake (the API already reports it running), and the refusal then
|
|
156
159
|
* surfaces. It throws when the wake fails (OperationFailedError) or outlasts `timeoutMs` (OperationTimeoutError).
|
|
157
160
|
* Workspace.cell() supplies `workspace.wake()`. A call refused with `workspace_not_running` wakes the workspace and is
|
|
158
|
-
* retried; a refused call was never executed
|
|
159
|
-
* the refusal instead. A wake that put a new token into this client's manager (the held resume
|
|
161
|
+
* retried; a refused call was never executed, so the retry cannot duplicate it. `null` surfaces
|
|
162
|
+
* the refusal instead. A wake that put a new token into this client's manager (the held resume;
|
|
160
163
|
* `workspace.wake({ agentLabel, tools })`) is retried with it; otherwise the token is replaced before the retry.
|
|
161
164
|
*/
|
|
162
165
|
wake?: ((timeoutMs: number, signal?: AbortSignal) => Promise<boolean | void>) | null;
|
|
@@ -174,7 +177,7 @@ export interface CellClientOptions {
|
|
|
174
177
|
/** Internal: see CAPTURE_BARRIER. */
|
|
175
178
|
[CAPTURE_BARRIER]?: (() => Promise<unknown> | undefined) | undefined;
|
|
176
179
|
/**
|
|
177
|
-
* The workspace's mode
|
|
180
|
+
* The workspace's mode, or a function returning it (undefined: unknown). When known, calls the mode
|
|
178
181
|
* does not have fail at once with NotSupportedForModeError (`local` true) instead of a request: exec sessions, PTY,
|
|
179
182
|
* processes, version control, browser and changes on a file-first workspace; executions and `ifTreeRevision` on a
|
|
180
183
|
* processful one. Workspace.cell() supplies the workspace's.
|
|
@@ -183,7 +186,7 @@ export interface CellClientOptions {
|
|
|
183
186
|
/** Called with every tree revision a response reports (`X-Tree-Revision`, execution results, mismatch refusals). */
|
|
184
187
|
onTreeRevision?: (revision: number) => void;
|
|
185
188
|
}
|
|
186
|
-
/** `ifTreeRevision` (file-first workspaces
|
|
189
|
+
/** `ifTreeRevision` (file-first workspaces): the tree revision a mutating files call applies to. */
|
|
187
190
|
export interface TreeRevisionOptions {
|
|
188
191
|
/**
|
|
189
192
|
* Sent as `If-Match`: the call applies only while the tree is at this revision (`workspace.treeRevision`, or a
|
|
@@ -197,7 +200,7 @@ export interface TreeRevisionOptions {
|
|
|
197
200
|
interface TransitionOptions {
|
|
198
201
|
/** Wake a suspended workspace for this call (default true; exec output follow reconnects pass false). */
|
|
199
202
|
wake?: boolean;
|
|
200
|
-
/** Wait out `workspace_busy` (default true; the wake hint passes false: it
|
|
203
|
+
/** Wait out `workspace_busy` (default true; the wake hint passes false: it never waits). */
|
|
201
204
|
busy?: boolean;
|
|
202
205
|
}
|
|
203
206
|
export declare const DEFAULT_TRANSITION_TIMEOUT_MS = 120000;
|
|
@@ -245,7 +248,7 @@ export interface RunOptions {
|
|
|
245
248
|
/** Output reconnect attempts after a dropped stream (default 10). */
|
|
246
249
|
maxReconnects?: number;
|
|
247
250
|
/**
|
|
248
|
-
* Names of customer secrets
|
|
251
|
+
* Names of customer secrets the cell injects as environment variables `NAME=value` of this
|
|
249
252
|
* process only (sent as `secret_refs`). Values are resolved at session start with this workspace's tool token and
|
|
250
253
|
* never returned. A name the caller may not use refuses the whole start with 403 `forbidden`
|
|
251
254
|
* (details.reason `secret_not_available`, details.names) and nothing runs; a name also present in `env` is 422.
|
|
@@ -274,7 +277,7 @@ export declare class CellClient {
|
|
|
274
277
|
*/
|
|
275
278
|
close(): void;
|
|
276
279
|
/**
|
|
277
|
-
* One authorized request. Refreshes the token once on stale_epoch / 401. Lifecycle transitions
|
|
280
|
+
* One authorized request. Refreshes the token once on stale_epoch / 401. Lifecycle transitions,
|
|
278
281
|
* bounded in total by `transitionTimeoutMs`: a call refused with `workspace_busy` is retried after `Retry-After`; one
|
|
279
282
|
* refused with `workspace_not_running` (or whose token cannot be minted because the workspace is not running) wakes
|
|
280
283
|
* the workspace through `wake`, given the time left, and is retried with a fresh token, at most 3 wakes per call.
|
|
@@ -296,6 +299,14 @@ export declare class CellClient {
|
|
|
296
299
|
follow?: boolean;
|
|
297
300
|
signal?: AbortSignal;
|
|
298
301
|
}) => Promise<AsyncGenerator<OutputEvent>>;
|
|
302
|
+
/** Write at the acknowledged offset (initially 0); a repeated identical last frame cannot duplicate input.
|
|
303
|
+
* At most 64 KiB per call. A partial acknowledgement requires continuing from the returned offset.
|
|
304
|
+
* Start with stdin_open: true. close sends EOF after this frame is fully accepted. */
|
|
305
|
+
input: (sessionId: string, data: string | Uint8Array, opts: {
|
|
306
|
+
offset: number;
|
|
307
|
+
close?: boolean;
|
|
308
|
+
signal?: AbortSignal;
|
|
309
|
+
}) => Promise<ExecInputResult>;
|
|
299
310
|
signal: (sessionId: string, signal: Signal, onlyLeader?: boolean) => Promise<ExecSession>;
|
|
300
311
|
cancel: (sessionId: string, graceMs?: number) => Promise<ExecSession>;
|
|
301
312
|
/**
|
|
@@ -314,9 +325,9 @@ export declare class CellClient {
|
|
|
314
325
|
* the next revision (`treeRevision`, `changed`); processes, memory and files elsewhere do not survive it.
|
|
315
326
|
*
|
|
316
327
|
* The execution id (`executionId`, default a fresh `ex-<uuid>`) is the idempotency key: network failures and
|
|
317
|
-
* retryable 429/5xx answers (503 `no_execution_host`:
|
|
318
|
-
* id and request, at most `maxRetries` times, and an attempt that waits longer than
|
|
319
|
-
* with the same id. The cell answers 201 for the call that ran the command and 200 (`replayed`) with the recorded
|
|
328
|
+
* retryable 429/5xx answers (503 `no_execution_host`: the execution cannot be placed right now, with Retry-After) are
|
|
329
|
+
* retried with the same id and request, at most `maxRetries` times, and an attempt that waits longer than
|
|
330
|
+
* `attemptTimeoutMs` re-attaches with the same id. The cell answers 201 for the call that ran the command and 200 (`replayed`) with the recorded
|
|
320
331
|
* result otherwise, so the command runs at most once. `workspace_busy` (`execution_in_progress`: another execution
|
|
321
332
|
* holds the workspace) is waited out within `transitionTimeoutMs`; `execution_id_reused` (the id with another
|
|
322
333
|
* request) and validation errors are thrown at once. The SDK never retries with a new id: a result whose `state` is
|
|
@@ -376,7 +387,7 @@ export declare class CellClient {
|
|
|
376
387
|
/**
|
|
377
388
|
* Like `read()`, plus the file's size, `revision` (SHA-256 of the whole file, `X-File-Revision`, for regular files of
|
|
378
389
|
* 16 MiB or less; pass it as `expectedRevision` to `patch()`) and `servedFrom` (`disk` when a sleeping workspace was
|
|
379
|
-
* read from its disk without waking it
|
|
390
|
+
* read from its disk without waking it). 0.9.0+.
|
|
380
391
|
*/
|
|
381
392
|
readWithInfo: (path: string, opts?: {
|
|
382
393
|
offset?: number;
|
|
@@ -414,7 +425,7 @@ export declare class CellClient {
|
|
|
414
425
|
overwrite?: boolean;
|
|
415
426
|
} & TreeRevisionOptions) => Promise<FileInfo>;
|
|
416
427
|
/**
|
|
417
|
-
* Searches file contents under `path`, a directory or one file (
|
|
428
|
+
* Searches file contents under `path`, a directory or one file (0.9.0+): matching lines in path
|
|
418
429
|
* order, bounded by `maxMatches`, a 10 s budget and 4 MiB of results (`truncated`, `stop_reason` `max_matches`,
|
|
419
430
|
* `budget` or `max_bytes`). Binary files, symbolic links, special files and files above `maxFileBytes` are
|
|
420
431
|
* skipped. Read-only, so transient failures (e.g. 503 `host_capacity`) are retried; a suspended workspace whose
|
|
@@ -423,7 +434,7 @@ export declare class CellClient {
|
|
|
423
434
|
search: (path: string, pattern: string, opts?: FileSearchOptions) => Promise<FileSearchResponse>;
|
|
424
435
|
/**
|
|
425
436
|
* Applies text `edits` (each `oldText` must occur exactly once unless `replaceAll`; in order) or replaces the whole
|
|
426
|
-
* file with `content`, atomically and durably (
|
|
437
|
+
* file with `content`, atomically and durably (0.9.0+). With `expectedRevision` the file must
|
|
427
438
|
* still have that revision (`absent`: must not exist), else 409 `conflict` `revision_mismatch` with
|
|
428
439
|
* `details.current_revision`. Edits that do not apply: 422 `validation_failed` `edit_not_found` / `edit_ambiguous`
|
|
429
440
|
* (`details.index`), `edit_not_text`. Limits: a request of up to 7 MiB (else 413 `payload_too_large`; larger
|
|
@@ -437,11 +448,15 @@ export declare class CellClient {
|
|
|
437
448
|
} & TreeRevisionOptions) => Promise<FilePatchResult>;
|
|
438
449
|
};
|
|
439
450
|
/**
|
|
440
|
-
* `POST /wake-hint` (
|
|
451
|
+
* `POST /wake-hint` (0.9.0+): announces an imminent tool call so a hibernated workspace is restored
|
|
441
452
|
* ahead of it. It never wakes a suspended workspace, never waits out `workspace_busy` and is not retried: a suspended
|
|
442
453
|
* workspace answers 409 `workspace_not_running` (`Workspace.hint()` then wakes it in the background).
|
|
443
454
|
*/
|
|
444
455
|
wakeHint(signal?: AbortSignal): Promise<WakeHintResult>;
|
|
456
|
+
/** Read idle signals without recording activity or waking the workspace. */
|
|
457
|
+
idle(signal?: AbortSignal): Promise<IdleStatus>;
|
|
458
|
+
/** Declare work for seconds (1..the server maximum). Never shortens a previous keepalive. */
|
|
459
|
+
keepalive(seconds: number, signal?: AbortSignal): Promise<KeepaliveResult>;
|
|
445
460
|
/**
|
|
446
461
|
* One page of the workspace's changes against its template (needs the `files` tool). File-first workspaces: each
|
|
447
462
|
* execution's result lists what it changed (`changed`); this route is refused (NotSupportedForModeError).
|
package/dist/cell.js
CHANGED
|
@@ -89,7 +89,7 @@ async function parseJson(res, what) {
|
|
|
89
89
|
return JSON.parse(text);
|
|
90
90
|
}
|
|
91
91
|
catch {
|
|
92
|
-
throw new ShardfluxProtocolError(`${what}: response is not JSON`, res.status);
|
|
92
|
+
throw new ShardfluxProtocolError(`${what}: response is not JSON`, res.status, 'cell');
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
/** Delay before a retry of an execution: Retry-After (at most 30 s), else 0.5 s doubling to 8 s. */
|
|
@@ -200,7 +200,7 @@ export class CellClient {
|
|
|
200
200
|
this.#closer.abort(new DOMException('The workspace handle was closed.', 'AbortError'));
|
|
201
201
|
}
|
|
202
202
|
/**
|
|
203
|
-
* One authorized request. Refreshes the token once on stale_epoch / 401. Lifecycle transitions
|
|
203
|
+
* One authorized request. Refreshes the token once on stale_epoch / 401. Lifecycle transitions,
|
|
204
204
|
* bounded in total by `transitionTimeoutMs`: a call refused with `workspace_busy` is retried after `Retry-After`; one
|
|
205
205
|
* refused with `workspace_not_running` (or whose token cannot be minted because the workspace is not running) wakes
|
|
206
206
|
* the workspace through `wake`, given the time left, and is retried with a fresh token, at most 3 wakes per call.
|
|
@@ -262,7 +262,7 @@ export class CellClient {
|
|
|
262
262
|
const before = this.tokens.current;
|
|
263
263
|
if ((await this.#wake(left, signal)) === false)
|
|
264
264
|
throw err; // running per the API: nothing to wait for
|
|
265
|
-
// A held resume
|
|
265
|
+
// A held resume handed this client a token of the woken workspace: use it. Otherwise the
|
|
266
266
|
// old token is of the previous epoch: fetch a new one.
|
|
267
267
|
if (this.tokens.current === before)
|
|
268
268
|
this.tokens.invalidate();
|
|
@@ -282,7 +282,7 @@ export class CellClient {
|
|
|
282
282
|
return (text.length === 0 ? undefined : JSON.parse(text));
|
|
283
283
|
}
|
|
284
284
|
catch {
|
|
285
|
-
throw new ShardfluxProtocolError(`${method} ${path}: response is not JSON`, res.status);
|
|
285
|
+
throw new ShardfluxProtocolError(`${method} ${path}: response is not JSON`, res.status, 'cell');
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
async #bytes(method, path, init = {}) {
|
|
@@ -329,9 +329,20 @@ export class CellClient {
|
|
|
329
329
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
330
330
|
});
|
|
331
331
|
if (!res.body)
|
|
332
|
-
throw new ShardfluxProtocolError('exec output: empty body', res.status);
|
|
332
|
+
throw new ShardfluxProtocolError('exec output: empty body', res.status, 'cell');
|
|
333
333
|
return ndjson(res.body);
|
|
334
334
|
},
|
|
335
|
+
/** Write at the acknowledged offset (initially 0); a repeated identical last frame cannot duplicate input.
|
|
336
|
+
* At most 64 KiB per call. A partial acknowledgement requires continuing from the returned offset.
|
|
337
|
+
* Start with stdin_open: true. close sends EOF after this frame is fully accepted. */
|
|
338
|
+
input: (sessionId, data, opts) => {
|
|
339
|
+
const refusal = this.#noSessions('exec.input');
|
|
340
|
+
if (refusal)
|
|
341
|
+
return Promise.reject(refusal);
|
|
342
|
+
return this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/exec/{session_id}/stdin', { session_id: sessionId }), {
|
|
343
|
+
json: { data: b64(data), offset: opts.offset, close: opts.close ?? false }, ...(opts.signal ? { signal: opts.signal } : {}),
|
|
344
|
+
});
|
|
345
|
+
},
|
|
335
346
|
signal: (sessionId, signal, onlyLeader = false) => {
|
|
336
347
|
const refusal = this.#noSessions('exec.signal');
|
|
337
348
|
if (refusal)
|
|
@@ -404,9 +415,14 @@ export class CellClient {
|
|
|
404
415
|
const maxReconnects = opts.maxReconnects ?? 10;
|
|
405
416
|
for (;;) {
|
|
406
417
|
let exited;
|
|
418
|
+
const streamAbort = new AbortController();
|
|
419
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, streamAbort.signal]) : streamAbort.signal;
|
|
420
|
+
let drainTimer;
|
|
407
421
|
try {
|
|
408
|
-
const events = await this.exec.output(sessionId, { stdoutOffset: so, stderrOffset: se, follow: true,
|
|
422
|
+
const events = await this.exec.output(sessionId, { stdoutOffset: so, stderrOffset: se, follow: true, signal });
|
|
409
423
|
for await (const ev of events) {
|
|
424
|
+
if (exited)
|
|
425
|
+
continue;
|
|
410
426
|
if (ev.type === 'output' && ev.data !== undefined) {
|
|
411
427
|
const bytes = unb64(ev.data);
|
|
412
428
|
const start = ev.offset ?? (ev.stream === 'stderr' ? se : so);
|
|
@@ -426,7 +442,9 @@ export class CellClient {
|
|
|
426
442
|
}
|
|
427
443
|
else if (ev.type === 'exit') {
|
|
428
444
|
exited = ev.session ?? (await this.exec.get(sessionId));
|
|
429
|
-
|
|
445
|
+
// Consume the terminal HTTP framing so the connection can be reused.
|
|
446
|
+
// A peer that never closes after exit must not hold the completed run forever.
|
|
447
|
+
drainTimer = setTimeout(() => streamAbort.abort(), 250);
|
|
430
448
|
}
|
|
431
449
|
else if (ev.type === 'error' && ev.error) {
|
|
432
450
|
throw new ShardfluxApiError(502, ev.error, 'cell');
|
|
@@ -437,9 +455,12 @@ export class CellClient {
|
|
|
437
455
|
if (opts.signal?.aborted || this.closed)
|
|
438
456
|
throw e;
|
|
439
457
|
const transient = !(e instanceof ShardfluxApiError) || e.retryable;
|
|
440
|
-
if (!transient || reconnects >= maxReconnects)
|
|
458
|
+
if (!exited && (!transient || reconnects >= maxReconnects))
|
|
441
459
|
throw e;
|
|
442
460
|
}
|
|
461
|
+
finally {
|
|
462
|
+
clearTimeout(drainTimer);
|
|
463
|
+
}
|
|
443
464
|
if (exited) {
|
|
444
465
|
session = exited;
|
|
445
466
|
break;
|
|
@@ -449,7 +470,7 @@ export class CellClient {
|
|
|
449
470
|
throw this.#closer.signal.reason;
|
|
450
471
|
reconnects += 1;
|
|
451
472
|
if (reconnects > maxReconnects)
|
|
452
|
-
throw new ShardfluxProtocolError(`exec ${sessionId}: output stream kept dropping`, 0);
|
|
473
|
+
throw new ShardfluxProtocolError(`exec ${sessionId}: output stream kept dropping`, 0, 'cell');
|
|
453
474
|
await this.#opts.sleep(Math.min(2_000, 100 * 2 ** reconnects));
|
|
454
475
|
const now = await this.exec.get(sessionId);
|
|
455
476
|
if (now.state !== 'starting' && now.state !== 'running' && so >= now.stdout_size && se >= now.stderr_size) {
|
|
@@ -475,7 +496,7 @@ export class CellClient {
|
|
|
475
496
|
reconnects,
|
|
476
497
|
};
|
|
477
498
|
}
|
|
478
|
-
// ---- executions (file-first workspaces
|
|
499
|
+
// ---- executions (file-first workspaces) -----------------------------
|
|
479
500
|
executions = {
|
|
480
501
|
/**
|
|
481
502
|
* Runs argv (no shell; `['bash', '-lc', line]` for shell syntax) as an execution of this file-first workspace: a
|
|
@@ -483,9 +504,9 @@ export class CellClient {
|
|
|
483
504
|
* the next revision (`treeRevision`, `changed`); processes, memory and files elsewhere do not survive it.
|
|
484
505
|
*
|
|
485
506
|
* The execution id (`executionId`, default a fresh `ex-<uuid>`) is the idempotency key: network failures and
|
|
486
|
-
* retryable 429/5xx answers (503 `no_execution_host`:
|
|
487
|
-
* id and request, at most `maxRetries` times, and an attempt that waits longer than
|
|
488
|
-
* with the same id. The cell answers 201 for the call that ran the command and 200 (`replayed`) with the recorded
|
|
507
|
+
* retryable 429/5xx answers (503 `no_execution_host`: the execution cannot be placed right now, with Retry-After) are
|
|
508
|
+
* retried with the same id and request, at most `maxRetries` times, and an attempt that waits longer than
|
|
509
|
+
* `attemptTimeoutMs` re-attaches with the same id. The cell answers 201 for the call that ran the command and 200 (`replayed`) with the recorded
|
|
489
510
|
* result otherwise, so the command runs at most once. `workspace_busy` (`execution_in_progress`: another execution
|
|
490
511
|
* holds the workspace) is waited out within `transitionTimeoutMs`; `execution_id_reused` (the id with another
|
|
491
512
|
* request) and validation errors are thrown at once. The SDK never retries with a new id: a result whose `state` is
|
|
@@ -708,7 +729,7 @@ export class CellClient {
|
|
|
708
729
|
finish(new ShardfluxApiError(502, m.error, 'cell'));
|
|
709
730
|
}
|
|
710
731
|
});
|
|
711
|
-
ws.addEventListener('error', () => finish(new ShardfluxProtocolError('pty attach WebSocket failed', 0)));
|
|
732
|
+
ws.addEventListener('error', () => finish(new ShardfluxProtocolError('pty attach WebSocket failed', 0, 'cell')));
|
|
712
733
|
ws.addEventListener('close', () => finish());
|
|
713
734
|
if (this.closed)
|
|
714
735
|
onClose();
|
|
@@ -744,7 +765,7 @@ export class CellClient {
|
|
|
744
765
|
/**
|
|
745
766
|
* Like `read()`, plus the file's size, `revision` (SHA-256 of the whole file, `X-File-Revision`, for regular files of
|
|
746
767
|
* 16 MiB or less; pass it as `expectedRevision` to `patch()`) and `servedFrom` (`disk` when a sleeping workspace was
|
|
747
|
-
* read from its disk without waking it
|
|
768
|
+
* read from its disk without waking it). 0.9.0+.
|
|
748
769
|
*/
|
|
749
770
|
readWithInfo: async (path, opts = {}) => {
|
|
750
771
|
const url = this.#p('/v1/workspaces/{workspace_id}/files');
|
|
@@ -805,7 +826,7 @@ export class CellClient {
|
|
|
805
826
|
headers: this.#ifMatch('files.move', opts),
|
|
806
827
|
}),
|
|
807
828
|
/**
|
|
808
|
-
* Searches file contents under `path`, a directory or one file (
|
|
829
|
+
* Searches file contents under `path`, a directory or one file (0.9.0+): matching lines in path
|
|
809
830
|
* order, bounded by `maxMatches`, a 10 s budget and 4 MiB of results (`truncated`, `stop_reason` `max_matches`,
|
|
810
831
|
* `budget` or `max_bytes`). Binary files, symbolic links, special files and files above `maxFileBytes` are
|
|
811
832
|
* skipped. Read-only, so transient failures (e.g. 503 `host_capacity`) are retried; a suspended workspace whose
|
|
@@ -833,7 +854,7 @@ export class CellClient {
|
|
|
833
854
|
},
|
|
834
855
|
/**
|
|
835
856
|
* Applies text `edits` (each `oldText` must occur exactly once unless `replaceAll`; in order) or replaces the whole
|
|
836
|
-
* file with `content`, atomically and durably (
|
|
857
|
+
* file with `content`, atomically and durably (0.9.0+). With `expectedRevision` the file must
|
|
837
858
|
* still have that revision (`absent`: must not exist), else 409 `conflict` `revision_mismatch` with
|
|
838
859
|
* `details.current_revision`. Edits that do not apply: 422 `validation_failed` `edit_not_found` / `edit_ambiguous`
|
|
839
860
|
* (`details.index`), `edit_not_text`. Limits: a request of up to 7 MiB (else 413 `payload_too_large`; larger
|
|
@@ -865,17 +886,25 @@ export class CellClient {
|
|
|
865
886
|
},
|
|
866
887
|
};
|
|
867
888
|
/**
|
|
868
|
-
* `POST /wake-hint` (
|
|
889
|
+
* `POST /wake-hint` (0.9.0+): announces an imminent tool call so a hibernated workspace is restored
|
|
869
890
|
* ahead of it. It never wakes a suspended workspace, never waits out `workspace_busy` and is not retried: a suspended
|
|
870
891
|
* workspace answers 409 `workspace_not_running` (`Workspace.hint()` then wakes it in the background).
|
|
871
892
|
*/
|
|
872
893
|
wakeHint(signal) {
|
|
873
|
-
// Nothing of a file-first workspace sleeps: the cell would answer `resident
|
|
894
|
+
// Nothing of a file-first workspace sleeps: the cell would answer `resident`.
|
|
874
895
|
if (this.mode === 'file_first')
|
|
875
896
|
return Promise.resolve({ residency: 'resident' });
|
|
876
897
|
return this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/wake-hint'), { wake: false, busy: false, timeoutMs: 10_000, ...(signal ? { signal } : {}) });
|
|
877
898
|
}
|
|
878
|
-
|
|
899
|
+
/** Read idle signals without recording activity or waking the workspace. */
|
|
900
|
+
idle(signal) {
|
|
901
|
+
return this.#json('GET', this.#p('/v1/workspaces/{workspace_id}/idle'), { wake: false, ...(signal ? { signal } : {}) });
|
|
902
|
+
}
|
|
903
|
+
/** Declare work for seconds (1..the server maximum). Never shortens a previous keepalive. */
|
|
904
|
+
keepalive(seconds, signal) {
|
|
905
|
+
return this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/keepalive'), { json: { seconds }, wake: false, ...(signal ? { signal } : {}) });
|
|
906
|
+
}
|
|
907
|
+
// ---- changes against the template (layered workspaces) ------------------
|
|
879
908
|
/**
|
|
880
909
|
* One page of the workspace's changes against its template (needs the `files` tool). File-first workspaces: each
|
|
881
910
|
* execution's result lists what it changed (`changed`); this route is refused (NotSupportedForModeError).
|