@opengeni/sdk 0.46.0 → 0.48.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/README.md CHANGED
@@ -408,7 +408,7 @@ Every public endpoint group has typed methods:
408
408
  | Turn queue | `getQueue`, `moveQueueItem`, `editQueueItem`, `steerQueueItem`, `deleteQueueItem` |
409
409
  | Goal | `getGoal`, `updateGoal`, `pauseGoal`, `resumeGoal` |
410
410
  | Scheduled tasks | `createScheduledTask`, `listScheduledTasks`, `getScheduledTask`, `updateScheduledTask`, `pauseScheduledTask`, `resumeScheduledTask`, `triggerScheduledTask`, `deleteScheduledTask`, `listScheduledTaskRuns` |
411
- | Variable sets | `listVariable sets`, `createVariable set`, `getVariable set`, `updateVariable set`, `deleteVariable set`, `setVariable setVariable`, `deleteVariable setVariable` (values are write-only) |
411
+ | Variable sets | `listVariableSets`, `createVariableSet`, `getVariableSet`, `updateVariableSet`, `deleteVariableSet`, `setVariableSetVariable`, `deleteVariableSetVariable`; generic reads are metadata-only, while dedicated permissioned exact-value reads are part of the held client train |
412
412
  | Files | `uploadFile`, `beginFileUpload`, `completeFileUpload`, `getFile`, `createFileDownloadUrl` |
413
413
  | Documents | `createDocumentBase`, `listDocumentBases`, `getDocumentBase`, `addDocument`, `listDocuments`, `reindexDocument`, `searchDocuments`, `searchKnowledge` (effective organization + workspace + immutable initiating-user personal scope) |
414
414
  | Packs | `listPacks`, `registerPack`, `getPack`, `enablePack`, `deletePack`, `listPackInstallations` |
@@ -82,6 +82,9 @@ async function* streamSessionEvents(transport, options = {}) {
82
82
  const reconnect = options.reconnect ?? true;
83
83
  const baseDelayMs = options.reconnectDelayMs ?? 500;
84
84
  const maxDelayMs = options.maxReconnectDelayMs ?? 1e4;
85
+ const jitterRatio = options.reconnectJitterRatio ?? 0.2;
86
+ const beforeLiveTimeoutMs = options.beforeLiveTimeoutMs ?? 15e3;
87
+ const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? 45e3;
85
88
  const maxAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
86
89
  let cursor = options.after ?? 0;
87
90
  let failedAttempts = 0;
@@ -96,9 +99,15 @@ async function* streamSessionEvents(transport, options = {}) {
96
99
  everConnected = true;
97
100
  failedAttempts = 0;
98
101
  delayMs = baseDelayMs;
99
- await options.beforeLive?.();
102
+ try {
103
+ options.onOpen?.();
104
+ } catch {
105
+ }
106
+ await runBeforeLive(options.beforeLive, beforeLiveTimeoutMs, signal);
100
107
  options.onStateChange?.("live");
101
- for await (const message of parseSseStream(body)) {
108
+ for await (const message of parseSseStream(
109
+ withStreamInactivityTimeout(body, heartbeatTimeoutMs, signal)
110
+ )) {
102
111
  if (signal?.aborted) {
103
112
  return;
104
113
  }
@@ -122,7 +131,7 @@ async function* streamSessionEvents(transport, options = {}) {
122
131
  return;
123
132
  }
124
133
  if (cursor === cursorAtOpen) {
125
- await sleep(baseDelayMs, signal);
134
+ await sleep(jitteredDelay(baseDelayMs, jitterRatio), signal);
126
135
  }
127
136
  continue;
128
137
  } catch (error) {
@@ -139,10 +148,76 @@ async function* streamSessionEvents(transport, options = {}) {
139
148
  );
140
149
  }
141
150
  }
142
- await sleep(delayMs, signal);
151
+ await sleep(jitteredDelay(delayMs, jitterRatio), signal);
143
152
  delayMs = Math.min(Math.max(delayMs * 2, baseDelayMs), maxDelayMs);
144
153
  }
145
154
  }
155
+ async function runBeforeLive(beforeLive, timeoutMs, signal) {
156
+ if (!beforeLive) return;
157
+ let timer;
158
+ let abortListener;
159
+ const timeout = new Promise((_resolve, reject) => {
160
+ timer = setTimeout(
161
+ () => reject(new TypeError(`stream reconciliation timed out after ${timeoutMs}ms`)),
162
+ timeoutMs
163
+ );
164
+ if (signal) {
165
+ abortListener = () => reject(new DOMException("Aborted", "AbortError"));
166
+ signal.addEventListener("abort", abortListener, { once: true });
167
+ }
168
+ });
169
+ try {
170
+ await Promise.race([Promise.resolve().then(beforeLive), timeout]);
171
+ } finally {
172
+ if (timer !== void 0) clearTimeout(timer);
173
+ if (signal && abortListener) signal.removeEventListener("abort", abortListener);
174
+ }
175
+ }
176
+ function jitteredDelay(delayMs, ratio) {
177
+ if (delayMs <= 0 || ratio <= 0) return delayMs;
178
+ const boundedRatio = Math.min(ratio, 1);
179
+ const spread = delayMs * boundedRatio;
180
+ return Math.max(0, delayMs - spread + Math.random() * spread * 2);
181
+ }
182
+ function withStreamInactivityTimeout(stream, timeoutMs, signal) {
183
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
184
+ throw new RangeError("stream heartbeat timeout must be a positive safe integer");
185
+ }
186
+ const reader = stream.getReader();
187
+ return new ReadableStream({
188
+ pull: async (controller) => {
189
+ let timer;
190
+ let abortListener;
191
+ try {
192
+ const result = await Promise.race([
193
+ reader.read(),
194
+ new Promise((_resolve, reject) => {
195
+ timer = setTimeout(
196
+ () => reject(new TypeError(`event stream heartbeat timed out after ${timeoutMs}ms`)),
197
+ timeoutMs
198
+ );
199
+ if (signal) {
200
+ abortListener = () => reject(new DOMException("Aborted", "AbortError"));
201
+ signal.addEventListener("abort", abortListener, { once: true });
202
+ }
203
+ })
204
+ ]);
205
+ if (result.done) controller.close();
206
+ else controller.enqueue(result.value);
207
+ } catch (error) {
208
+ void reader.cancel(error).catch(() => {
209
+ });
210
+ controller.error(error);
211
+ } finally {
212
+ if (timer !== void 0) clearTimeout(timer);
213
+ if (signal && abortListener) signal.removeEventListener("abort", abortListener);
214
+ }
215
+ },
216
+ cancel: async (reason) => {
217
+ await reader.cancel(reason);
218
+ }
219
+ });
220
+ }
146
221
  async function* backfillEvents(transport, fromExclusive, toInclusive) {
147
222
  let cursor = fromExclusive;
148
223
  while (cursor < toInclusive) {
@@ -197,6 +272,9 @@ async function* streamWorkspaceControlEvents(transport, options = {}) {
197
272
  const reconnect = options.reconnect ?? true;
198
273
  const baseDelayMs = options.reconnectDelayMs ?? 500;
199
274
  const maxDelayMs = options.maxReconnectDelayMs ?? 1e4;
275
+ const jitterRatio = options.reconnectJitterRatio ?? 0.2;
276
+ const beforeLiveTimeoutMs = options.beforeLiveTimeoutMs ?? 15e3;
277
+ const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? 45e3;
200
278
  const maxAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
201
279
  let cursor = options.after ?? 0;
202
280
  let failures = 0;
@@ -211,9 +289,11 @@ async function* streamWorkspaceControlEvents(transport, options = {}) {
211
289
  everConnected = true;
212
290
  failures = 0;
213
291
  delayMs = baseDelayMs;
214
- await options.beforeLive?.();
292
+ await runBeforeLive(options.beforeLive, beforeLiveTimeoutMs, signal);
215
293
  options.onStateChange?.("live");
216
- for await (const message of parseSseStream(body)) {
294
+ for await (const message of parseSseStream(
295
+ withStreamInactivityTimeout(body, heartbeatTimeoutMs, signal)
296
+ )) {
217
297
  if (signal?.aborted) return;
218
298
  const event = parseWorkspaceControlEvent(message.data);
219
299
  if (!event || event.sequence <= cursor) continue;
@@ -221,7 +301,7 @@ async function* streamWorkspaceControlEvents(transport, options = {}) {
221
301
  yield event;
222
302
  }
223
303
  if (!reconnect) return;
224
- if (cursor === cursorAtOpen) await sleep2(baseDelayMs, signal);
304
+ if (cursor === cursorAtOpen) await sleep2(jitteredDelay(baseDelayMs, jitterRatio), signal);
225
305
  continue;
226
306
  } catch (error) {
227
307
  if (signal?.aborted || isAbortError(error)) return;
@@ -233,7 +313,7 @@ async function* streamWorkspaceControlEvents(transport, options = {}) {
233
313
  );
234
314
  }
235
315
  }
236
- await sleep2(delayMs, signal);
316
+ await sleep2(jitteredDelay(delayMs, jitterRatio), signal);
237
317
  delayMs = Math.min(Math.max(delayMs * 2, baseDelayMs), maxDelayMs);
238
318
  }
239
319
  }
@@ -421,8 +501,14 @@ var KNOWN_PERMISSIONS = [
421
501
  "connections:write",
422
502
  "environments:manage",
423
503
  "environments:use",
504
+ "variable-sets:list",
505
+ "variable-sets:read",
506
+ "variable-sets:write",
424
507
  "variable-sets:manage",
425
508
  "variable-sets:use",
509
+ "secrets:list",
510
+ "secrets:read",
511
+ "secrets:write",
426
512
  "mcp_servers:attach",
427
513
  "toolspace:call",
428
514
  "goals:manage",
@@ -438,6 +524,7 @@ var OPENGENI_API_CONTRACT_HEADER = "x-opengeni-api-contract";
438
524
  var OPENGENI_CORRELATION_HEADER = "x-opengeni-correlation-id";
439
525
  var RETAINED_OUTPUT_DEFAULT_PAGE_BYTES = 256 * 1024;
440
526
  var RETAINED_OUTPUT_MAX_PAGE_BYTES = 1024 * 1024;
527
+ var COMPUTER_SCREENSHOT_MAX_BYTES = 32 * 1024 * 1024;
441
528
  var KNOWN_USAGE_EVENT_TYPES = [
442
529
  "agent_run.created",
443
530
  "agent_run.completed",
@@ -465,6 +552,8 @@ var OpenGeniClient = class {
465
552
  baseUrl;
466
553
  options;
467
554
  fetchImpl;
555
+ readInFlight = /* @__PURE__ */ new Map();
556
+ readTrailing = /* @__PURE__ */ new Map();
468
557
  constructor(options) {
469
558
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
470
559
  this.options = options;
@@ -476,7 +565,11 @@ var OpenGeniClient = class {
476
565
  const correlationId = crypto.randomUUID();
477
566
  const form = new FormData();
478
567
  const filename = filenameForAudioMimeType(input.mimeType);
479
- const audio = input.audio instanceof File ? input.audio : input.audio instanceof Uint8Array ? new File([Uint8Array.from(input.audio)], filename, { type: input.mimeType }) : new File([input.audio], filename, { type: input.mimeType || input.audio.type });
568
+ const audio = input.audio instanceof File ? input.audio : input.audio instanceof Uint8Array ? new File([Uint8Array.from(input.audio)], filename, {
569
+ type: input.mimeType
570
+ }) : new File([input.audio], filename, {
571
+ type: input.mimeType || input.audio.type
572
+ });
480
573
  form.append("audio", audio, filename);
481
574
  form.append("mimeType", input.mimeType);
482
575
  if (input.durationSeconds !== void 0) {
@@ -486,7 +579,10 @@ var OpenGeniClient = class {
486
579
  try {
487
580
  response = await this.fetchImpl(this.url(`/v1/workspaces/${workspaceId}/transcriptions`), {
488
581
  method: "POST",
489
- headers: { ...this.headers(correlationId), Accept: "application/json" },
582
+ headers: {
583
+ ...this.headers(correlationId),
584
+ Accept: "application/json"
585
+ },
490
586
  body: form,
491
587
  ...input.signal ? { signal: input.signal } : {}
492
588
  });
@@ -495,7 +591,11 @@ var OpenGeniClient = class {
495
591
  throw mutationTransportError(correlationId);
496
592
  }
497
593
  assertApiContractResponse(response);
498
- if (!response.ok) throw await apiErrorFromResponse(response, { method: "POST", correlationId });
594
+ if (!response.ok)
595
+ throw await apiErrorFromResponse(response, {
596
+ method: "POST",
597
+ correlationId
598
+ });
499
599
  await assertJsonResponse(response, { method: "POST", correlationId });
500
600
  let body;
501
601
  try {
@@ -580,7 +680,11 @@ var OpenGeniClient = class {
580
680
  throw mutationTransportError(correlationId);
581
681
  }
582
682
  assertApiContractResponse(response);
583
- if (!response.ok) throw await apiErrorFromResponse(response, { method: "PUT", correlationId });
683
+ if (!response.ok)
684
+ throw await apiErrorFromResponse(response, {
685
+ method: "PUT",
686
+ correlationId
687
+ });
584
688
  await assertJsonResponse(response, { method: "PUT", correlationId });
585
689
  const body = await response.json().catch(() => null);
586
690
  if (!isUploadTranscriptionRecordingChunkResponse(body)) {
@@ -649,11 +753,9 @@ var OpenGeniClient = class {
649
753
  request
650
754
  );
651
755
  }
652
- async getSession(workspaceId, sessionId) {
653
- return await this.requestJson(
654
- "GET",
655
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}`
656
- );
756
+ async getSession(workspaceId, sessionId, options = {}) {
757
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}`;
758
+ return await this.singleFlightRead(path, () => this.requestJson("GET", path), options);
657
759
  }
658
760
  async updateSession(workspaceId, sessionId, request) {
659
761
  return await this.requestJson(
@@ -746,10 +848,34 @@ var OpenGeniClient = class {
746
848
  );
747
849
  }
748
850
  async getSessionLineage(workspaceId, sessionId) {
749
- return await this.requestJson(
750
- "GET",
751
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/lineage`
752
- );
851
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}/lineage`;
852
+ return await this.singleFlightRead(
853
+ path,
854
+ () => this.requestJson("GET", path)
855
+ );
856
+ }
857
+ singleFlightRead(key, read, options = {}) {
858
+ const existing = this.readInFlight.get(key);
859
+ if (existing) {
860
+ if (!options.fresh) return existing;
861
+ const queued = this.readTrailing.get(key);
862
+ if (queued) return queued;
863
+ const trailing = existing.then(
864
+ () => this.singleFlightRead(key, read),
865
+ () => this.singleFlightRead(key, read)
866
+ );
867
+ this.readTrailing.set(key, trailing);
868
+ const clear = () => {
869
+ if (this.readTrailing.get(key) === trailing) this.readTrailing.delete(key);
870
+ };
871
+ void trailing.then(clear, clear);
872
+ return trailing;
873
+ }
874
+ const promise = read().finally(() => {
875
+ if (this.readInFlight.get(key) === promise) this.readInFlight.delete(key);
876
+ });
877
+ this.readInFlight.set(key, promise);
878
+ return promise;
753
879
  }
754
880
  /** Negotiate one server-mediated connected-Codex GPT-Live V3 WebRTC call. */
755
881
  async negotiateCodexRealtimeWebrtc(workspaceId, sessionId, request, options = {}) {
@@ -826,7 +952,9 @@ var OpenGeniClient = class {
826
952
  }
827
953
  /** Newest turn that durably emitted `turn.started`, or null before any admission. */
828
954
  async getLatestStartedTurn(workspaceId, sessionId) {
829
- const turns = await this.listTurns(workspaceId, sessionId, { latestStarted: true });
955
+ const turns = await this.listTurns(workspaceId, sessionId, {
956
+ latestStarted: true
957
+ });
830
958
  return turns[0] ?? null;
831
959
  }
832
960
  // --- Bring-your-own-compute: Machines dashboard + metrics (M10) ------------
@@ -860,6 +988,19 @@ var OpenGeniClient = class {
860
988
  );
861
989
  return response.samples;
862
990
  }
991
+ /**
992
+ * Remove one connected self-hosted machine enrollment. The control-plane
993
+ * operation works while the agent is offline, revokes future reconnects,
994
+ * retains history, and returns a typed blocker when active route/lease or
995
+ * recovery dependencies make removal unsafe. `idempotencyKey` is replay-safe.
996
+ */
997
+ async removeEnrollment(workspaceId, enrollmentId, request = {}) {
998
+ return await this.requestJson(
999
+ "POST",
1000
+ `/v1/workspaces/${workspaceId}/enrollments/${enrollmentId}/revoke`,
1001
+ request
1002
+ );
1003
+ }
863
1004
  // --- Self-hosted enrollment UX (design 11) --------------------------------
864
1005
  /**
865
1006
  * Resolve a pending device-enrollment flow by its user_code for the click-Grant
@@ -1144,9 +1285,10 @@ var OpenGeniClient = class {
1144
1285
  }
1145
1286
  // --- Turn queue ------------------------------------------------------------
1146
1287
  async getQueue(workspaceId, sessionId) {
1147
- return await this.requestJson(
1148
- "GET",
1149
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue`
1288
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue`;
1289
+ return await this.singleFlightRead(
1290
+ path,
1291
+ () => this.requestJson("GET", path)
1150
1292
  );
1151
1293
  }
1152
1294
  async moveQueueItem(workspaceId, sessionId, turnId, request) {
@@ -1314,10 +1456,8 @@ var OpenGeniClient = class {
1314
1456
  // --- Goals -------------------------------------------------------------------
1315
1457
  /** The session's goal. 404s when the session never had one. */
1316
1458
  async getGoal(workspaceId, sessionId) {
1317
- return await this.requestJson(
1318
- "GET",
1319
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`
1320
- );
1459
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`;
1460
+ return await this.singleFlightRead(path, () => this.requestJson("GET", path));
1321
1461
  }
1322
1462
  async updateGoal(workspaceId, sessionId, request) {
1323
1463
  return await this.requestJson(
@@ -1377,6 +1517,16 @@ var OpenGeniClient = class {
1377
1517
  options
1378
1518
  );
1379
1519
  }
1520
+ /** FileSystem: hydrate several independent directories behind one sandbox lease. */
1521
+ async fsListBatch(workspaceId, sessionId, request, options = {}) {
1522
+ return await this.requestJson(
1523
+ "POST",
1524
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/list-batch`,
1525
+ request,
1526
+ {},
1527
+ options
1528
+ );
1529
+ }
1380
1530
  /** FileSystem: read a file (text or base64; binary-safe, size-capped). */
1381
1531
  async fsRead(workspaceId, sessionId, request, options = {}) {
1382
1532
  return await this.requestJson(
@@ -1439,6 +1589,16 @@ var OpenGeniClient = class {
1439
1589
  options
1440
1590
  );
1441
1591
  }
1592
+ /** Git: read status and optional diffs for several repositories behind one sandbox lease. */
1593
+ async gitReadBatch(workspaceId, sessionId, request, options = {}) {
1594
+ return await this.requestJson(
1595
+ "POST",
1596
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/read-batch`,
1597
+ request,
1598
+ {},
1599
+ options
1600
+ );
1601
+ }
1442
1602
  /** Git: commit log. */
1443
1603
  async gitLog(workspaceId, sessionId, request = {}) {
1444
1604
  return await this.requestJson(
@@ -1911,7 +2071,8 @@ var OpenGeniClient = class {
1911
2071
  );
1912
2072
  }
1913
2073
  // --- VariableSets --------------------------------------------------------------
1914
- // Variable values are write-only: reads return name/version metadata only.
2074
+ // Generic reads return name/version metadata only. Plaintext uses one
2075
+ // dedicated permissioned endpoint.
1915
2076
  async listVariableSets(workspaceId) {
1916
2077
  return await this.requestJson(
1917
2078
  "GET",
@@ -1931,6 +2092,12 @@ var OpenGeniClient = class {
1931
2092
  `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}`
1932
2093
  );
1933
2094
  }
2095
+ async getVariableSetVariable(workspaceId, variableSetId, name) {
2096
+ return await this.requestJson(
2097
+ "GET",
2098
+ `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}/variables/${encodeURIComponent(name)}`
2099
+ );
2100
+ }
1934
2101
  async updateVariableSet(workspaceId, variableSetId, request) {
1935
2102
  return await this.requestJson(
1936
2103
  "PATCH",
@@ -1944,7 +2111,7 @@ var OpenGeniClient = class {
1944
2111
  `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}`
1945
2112
  );
1946
2113
  }
1947
- /** Create or rotate a variable. The value never comes back on any read. */
2114
+ /** Create or rotate a variable. Generic reads never return its value. */
1948
2115
  async setVariableSetVariable(workspaceId, variableSetId, name, value) {
1949
2116
  return await this.requestJson(
1950
2117
  "PUT",
@@ -2076,18 +2243,23 @@ var OpenGeniClient = class {
2076
2243
  }
2077
2244
  // --- Files -----------------------------------------------------------------------
2078
2245
  /** Step 1 of the upload flow: returns the pre-signed PUT target. */
2079
- async beginFileUpload(workspaceId, request) {
2246
+ async beginFileUpload(workspaceId, request, options = {}) {
2080
2247
  return await this.requestJson(
2081
2248
  "POST",
2082
2249
  `/v1/workspaces/${workspaceId}/files/uploads`,
2083
- request
2250
+ request,
2251
+ {},
2252
+ options
2084
2253
  );
2085
2254
  }
2086
2255
  /** Step 3 of the upload flow: server verifies the object and marks it ready. */
2087
- async completeFileUpload(workspaceId, uploadId) {
2256
+ async completeFileUpload(workspaceId, uploadId, options = {}) {
2088
2257
  const response = await this.requestJson(
2089
2258
  "POST",
2090
- `/v1/workspaces/${workspaceId}/files/uploads/${uploadId}/complete`
2259
+ `/v1/workspaces/${workspaceId}/files/uploads/${uploadId}/complete`,
2260
+ void 0,
2261
+ {},
2262
+ options
2091
2263
  );
2092
2264
  return response.file;
2093
2265
  }
@@ -2097,35 +2269,68 @@ var OpenGeniClient = class {
2097
2269
  * -> complete. Returns the ready `FileAsset`.
2098
2270
  */
2099
2271
  async uploadFile(workspaceId, input) {
2272
+ if (input.timeoutMs !== void 0 && (!Number.isFinite(input.timeoutMs) || input.timeoutMs <= 0)) {
2273
+ throw new Error("File upload timeout must be a positive number");
2274
+ }
2275
+ const withTimeout = async (timeoutMs, operation) => {
2276
+ const controller = new AbortController();
2277
+ let timeout;
2278
+ const timedOut = new Promise((_resolve, reject) => {
2279
+ timeout = setTimeout(() => {
2280
+ controller.abort();
2281
+ reject(new Error("File upload timed out. Retry the upload."));
2282
+ }, timeoutMs);
2283
+ });
2284
+ try {
2285
+ return await Promise.race([operation(controller.signal), timedOut]);
2286
+ } finally {
2287
+ if (timeout !== void 0) clearTimeout(timeout);
2288
+ }
2289
+ };
2100
2290
  const body = input.data instanceof Uint8Array ? new Blob([input.data.slice()]) : input.data instanceof ArrayBuffer ? input.data.slice(0) : input.data;
2101
2291
  const sizeBytes = typeof body === "string" ? new TextEncoder().encode(body).byteLength : body instanceof Blob ? body.size : body.byteLength;
2102
2292
  const sha256 = input.sha256 ?? await sha256ForUpload(body);
2103
- const upload = await this.beginFileUpload(workspaceId, {
2104
- filename: input.filename,
2105
- contentType: input.contentType,
2106
- sizeBytes,
2107
- sha256
2108
- });
2109
- const putResponse = await this.fetchImpl(upload.putUrl, {
2110
- method: "PUT",
2111
- // Signed object-storage URLs carry their own short-lived authority.
2112
- // Browser cookies and HTTP auth must never accompany this cross-origin
2113
- // request: credentialed fetches are incompatible with wildcard CORS and
2114
- // can leak ambient credentials to a caller-selected storage endpoint.
2115
- credentials: "omit",
2116
- // The backend's requiredHeaders already carry the canonical lowercase
2117
- // `content-type` for every storage backend (Azure/S3/GCS). Do NOT also set
2118
- // a `Content-Type` key here: WHATWG Headers treats the two casings as the
2119
- // same header and comma-joins their values (e.g. "text/plain, text/plain"),
2120
- // which the object store persists verbatim and COMPLETE then rejects (422),
2121
- // and which breaks S3's presigned-URL signature.
2122
- headers: { ...upload.requiredHeaders },
2123
- body
2124
- });
2293
+ const upload = await withTimeout(
2294
+ 3e4,
2295
+ async (signal) => await this.beginFileUpload(
2296
+ workspaceId,
2297
+ {
2298
+ filename: input.filename,
2299
+ contentType: input.contentType,
2300
+ sizeBytes,
2301
+ sha256
2302
+ },
2303
+ { signal }
2304
+ )
2305
+ );
2306
+ const transferTimeoutMs = input.timeoutMs ?? Math.max(12e4, Math.ceil(sizeBytes / (256 * 1024)) * 1e3);
2307
+ const putResponse = await withTimeout(
2308
+ transferTimeoutMs,
2309
+ async (signal) => await this.fetchImpl(upload.putUrl, {
2310
+ method: "PUT",
2311
+ // Signed object-storage URLs carry their own short-lived authority.
2312
+ // Browser cookies and HTTP auth must never accompany this cross-origin
2313
+ // request: credentialed fetches are incompatible with wildcard CORS and
2314
+ // can leak ambient credentials to a caller-selected storage endpoint.
2315
+ credentials: "omit",
2316
+ // The backend's requiredHeaders already carry the canonical lowercase
2317
+ // `content-type` for every storage backend (Azure/S3/GCS). Do NOT also set
2318
+ // a `Content-Type` key here: WHATWG Headers treats the two casings as the
2319
+ // same header and comma-joins their values (e.g. "text/plain, text/plain"),
2320
+ // which the object store persists verbatim and COMPLETE then rejects (422),
2321
+ // and which breaks S3's presigned-URL signature.
2322
+ headers: { ...upload.requiredHeaders },
2323
+ body,
2324
+ signal
2325
+ })
2326
+ );
2125
2327
  if (!putResponse.ok) {
2126
2328
  throw await apiErrorFromResponse(putResponse, { method: "PUT" });
2127
2329
  }
2128
- return await this.completeFileUpload(workspaceId, upload.uploadId);
2330
+ return await withTimeout(
2331
+ 3e4,
2332
+ async (signal) => await this.completeFileUpload(workspaceId, upload.uploadId, { signal })
2333
+ );
2129
2334
  }
2130
2335
  async getFile(workspaceId, fileId) {
2131
2336
  return await this.requestJson(
@@ -2145,22 +2350,83 @@ var OpenGeniClient = class {
2145
2350
  * deliberately does not use the ordinary signed file-download URL.
2146
2351
  */
2147
2352
  async getRetainedArtifactContent(workspaceId, artifactId, options = {}) {
2353
+ return await this.getRetainedArtifactContentAtPath(
2354
+ `/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`,
2355
+ options
2356
+ );
2357
+ }
2358
+ async getSessionRetainedArtifact(workspaceId, sessionId, artifactId) {
2359
+ return await this.requestJson(
2360
+ "GET",
2361
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/artifacts/${artifactId}`
2362
+ );
2363
+ }
2364
+ async getSessionRetainedArtifactContent(workspaceId, sessionId, artifactId, options = {}) {
2365
+ return await this.getRetainedArtifactContentAtPath(
2366
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/artifacts/${artifactId}/content`,
2367
+ options
2368
+ );
2369
+ }
2370
+ /** Assemble one retained screenshot from bounded authenticated API ranges. */
2371
+ async downloadRetainedScreenshot(workspaceId, sessionId, artifactId, options = {}) {
2372
+ const maxRetries = options.maxRetries ?? 2;
2373
+ if (!Number.isInteger(maxRetries) || maxRetries < 0 || maxRetries > 3) {
2374
+ throw new RangeError("retained screenshot maxRetries must be an integer from 0 to 3");
2375
+ }
2376
+ const metadata = await this.getSessionRetainedArtifact(workspaceId, sessionId, artifactId);
2377
+ if (!metadata.available) return { metadata, bytes: null };
2378
+ if (metadata.kind !== "computer_screenshot" || metadata.contentType !== "image/png" || !metadata.dimensions || metadata.originalBytes <= 0 || metadata.originalBytes > COMPUTER_SCREENSHOT_MAX_BYTES) {
2379
+ throw new OpenGeniApiError(502, "retained screenshot metadata is invalid");
2380
+ }
2381
+ const bytes = new Uint8Array(metadata.originalBytes);
2382
+ const pageBytes = Math.min(metadata.retrieval.maxRangeBytes, RETAINED_OUTPUT_MAX_PAGE_BYTES);
2383
+ if (!Number.isSafeInteger(pageBytes) || pageBytes <= 0) {
2384
+ throw new OpenGeniApiError(502, "retained screenshot range metadata is invalid");
2385
+ }
2386
+ for (let start = 0; start < bytes.byteLength; start += pageBytes) {
2387
+ options.signal?.throwIfAborted();
2388
+ const end = Math.min(start + pageBytes, bytes.byteLength) - 1;
2389
+ let page = null;
2390
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
2391
+ try {
2392
+ page = await this.getSessionRetainedArtifactContent(workspaceId, sessionId, artifactId, {
2393
+ range: `bytes=${start}-${end}`,
2394
+ ...options.signal ? { signal: options.signal } : {}
2395
+ });
2396
+ break;
2397
+ } catch (error) {
2398
+ options.signal?.throwIfAborted();
2399
+ if (attempt >= maxRetries || error instanceof OpenGeniApiError && error.status >= 400 && error.status < 500) {
2400
+ throw error;
2401
+ }
2402
+ }
2403
+ }
2404
+ if (!page) throw new OpenGeniApiError(502, "retained screenshot range retry exhausted");
2405
+ const expectedLength = end - start + 1;
2406
+ if (page.status !== 206 || page.contentType !== metadata.contentType || page.contentLength !== expectedLength || page.contentRange !== `bytes ${start}-${end}/${metadata.originalBytes}`) {
2407
+ throw new OpenGeniApiError(502, "retained screenshot range response is invalid");
2408
+ }
2409
+ bytes.set(page.bytes, start);
2410
+ }
2411
+ if (await sha256Hex(bytes) !== metadata.sha256) {
2412
+ throw new OpenGeniApiError(502, "retained screenshot checksum mismatch");
2413
+ }
2414
+ return { metadata, bytes };
2415
+ }
2416
+ async getRetainedArtifactContentAtPath(path, options) {
2148
2417
  if (options.range && (options.range.length > 128 || /[^\x20-\x7e]/.test(options.range))) {
2149
2418
  throw new RangeError("retained artifact range must be at most 128 printable ASCII bytes");
2150
2419
  }
2151
2420
  const correlationId = crypto.randomUUID();
2152
- const response = await this.fetchImpl(
2153
- this.url(`/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`),
2154
- {
2155
- method: "GET",
2156
- headers: {
2157
- ...this.headers(correlationId),
2158
- Accept: "application/octet-stream",
2159
- ...options.range ? { Range: options.range } : {}
2160
- },
2161
- ...options.signal ? { signal: options.signal } : {}
2162
- }
2163
- );
2421
+ const response = await this.fetchImpl(this.url(path), {
2422
+ method: "GET",
2423
+ headers: {
2424
+ ...this.headers(correlationId),
2425
+ Accept: "application/octet-stream",
2426
+ ...options.range ? { Range: options.range } : {}
2427
+ },
2428
+ ...options.signal ? { signal: options.signal } : {}
2429
+ });
2164
2430
  try {
2165
2431
  assertApiContractResponse(response);
2166
2432
  } catch (error) {
@@ -2843,7 +3109,10 @@ function isTranscribeAudioResponse(value) {
2843
3109
  function isUploadTranscriptionRecordingChunkResponse(value) {
2844
3110
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2845
3111
  const record = value;
2846
- if (!isTranscriptionRecordingResponse({ recording: record.recording, segments: [] })) {
3112
+ if (!isTranscriptionRecordingResponse({
3113
+ recording: record.recording,
3114
+ segments: []
3115
+ })) {
2847
3116
  return false;
2848
3117
  }
2849
3118
  if (!record.chunk || typeof record.chunk !== "object" || Array.isArray(record.chunk)) {
@@ -2947,6 +3216,11 @@ async function sha256ForUpload(body) {
2947
3216
  const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
2948
3217
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2949
3218
  }
3219
+ async function sha256Hex(bytes) {
3220
+ const owned = Uint8Array.from(bytes);
3221
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", owned.buffer);
3222
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
3223
+ }
2950
3224
  async function cancelResponseBody(response, reason) {
2951
3225
  await response.body?.cancel(reason).catch(() => void 0);
2952
3226
  }
@@ -3239,4 +3513,4 @@ export {
3239
3513
  authorizeTranscriptionAdapter,
3240
3514
  createTranscriptionSessionRequest
3241
3515
  };
3242
- //# sourceMappingURL=chunk-QKV5OCLJ.js.map
3516
+ //# sourceMappingURL=chunk-MBGBLVUV.js.map