@opengeni/sdk 0.44.6 → 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/src/client.ts CHANGED
@@ -101,6 +101,8 @@ import type {
101
101
  MachinesResponse,
102
102
  MetricSample,
103
103
  MachineMetricsSeriesResponse,
104
+ RemoveEnrollmentRequest,
105
+ RemoveEnrollmentResponse,
104
106
  // Bring-your-own-compute: the user-authenticated active-sandbox swap (M7).
105
107
  SwapActiveSandboxRequest,
106
108
  SwapActiveSandboxResponse,
@@ -108,6 +110,8 @@ import type {
108
110
  PackInstallation,
109
111
  LatencyMode,
110
112
  ReasoningEffort,
113
+ RetainedScreenshotDownload,
114
+ RetainedScreenshotDownloadOptions,
111
115
  RetainedArtifactContent,
112
116
  RetainedArtifactContentOptions,
113
117
  RetainedArtifactMetadata,
@@ -159,6 +163,8 @@ import type {
159
163
  // Channel-A structured services (P4.4).
160
164
  FsListRequest,
161
165
  FsListResponse,
166
+ FsListBatchRequest,
167
+ FsListBatchResponse,
162
168
  FsReadRequest,
163
169
  FsReadResponse,
164
170
  FsWriteRequest,
@@ -173,6 +179,8 @@ import type {
173
179
  GitStatusResponse,
174
180
  GitDiffRequest,
175
181
  GitDiffResponse,
182
+ GitReadBatchRequest,
183
+ GitReadBatchResponse,
176
184
  GitLogRequest,
177
185
  GitLogResponse,
178
186
  GitShowRequest,
@@ -189,6 +197,9 @@ import type {
189
197
  PtyCloseRequest,
190
198
  ToolRef,
191
199
  TranscribeAudioResponse,
200
+ TranscriptionRecordingListResponse,
201
+ TranscriptionRecordingResponse,
202
+ UploadTranscriptionRecordingChunkResponse,
192
203
  UpdateConnectionRequest,
193
204
  UpdateKnowledgeMemoryRequest,
194
205
  UpdateScheduledTaskRequest,
@@ -203,6 +214,7 @@ import type {
203
214
  SetWorkspaceDefaultRigRequest,
204
215
  UploadFileInput,
205
216
  VariableSet,
217
+ VariableSetSecret,
206
218
  VariableSetVariableMetadata,
207
219
  Rig,
208
220
  RigVersion,
@@ -261,6 +273,7 @@ import {
261
273
  OPENGENI_API_CONTRACT_HEADER,
262
274
  OPENGENI_API_CONTRACT_REVISION,
263
275
  OPENGENI_CORRELATION_HEADER,
276
+ COMPUTER_SCREENSHOT_MAX_BYTES,
264
277
  RETAINED_OUTPUT_MAX_PAGE_BYTES,
265
278
  } from "./types";
266
279
 
@@ -329,6 +342,28 @@ export type TranscribeAudioInput = {
329
342
  signal?: AbortSignal | undefined;
330
343
  };
331
344
 
345
+ export type CreateTranscriptionRecordingInput = {
346
+ recordingId: string;
347
+ mimeType: string;
348
+ signal?: AbortSignal | undefined;
349
+ };
350
+
351
+ export type UploadTranscriptionRecordingChunkInput = {
352
+ audio: Blob | File | Uint8Array;
353
+ mimeType: string;
354
+ sha256: string;
355
+ startMilliseconds: number;
356
+ durationMilliseconds: number;
357
+ signal?: AbortSignal | undefined;
358
+ };
359
+
360
+ export type FinalizeTranscriptionRecordingInput = {
361
+ chunkCount: number;
362
+ totalBytes: number;
363
+ totalDurationMilliseconds: number;
364
+ signal?: AbortSignal | undefined;
365
+ };
366
+
332
367
  /**
333
368
  * Typed client for the OpenGeni public API. Framework-agnostic: only needs
334
369
  * WHATWG `fetch` + streams, so it runs in Node 18+, Bun, Deno, browsers, and
@@ -338,6 +373,8 @@ export class OpenGeniClient {
338
373
  private readonly baseUrl: string;
339
374
  private readonly options: OpenGeniClientOptions;
340
375
  private readonly fetchImpl: FetchLike;
376
+ private readonly readInFlight = new Map<string, Promise<unknown>>();
377
+ private readonly readTrailing = new Map<string, Promise<unknown>>();
341
378
 
342
379
  constructor(options: OpenGeniClientOptions) {
343
380
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
@@ -360,8 +397,12 @@ export class OpenGeniClient {
360
397
  input.audio instanceof File
361
398
  ? input.audio
362
399
  : input.audio instanceof Uint8Array
363
- ? new File([Uint8Array.from(input.audio)], filename, { type: input.mimeType })
364
- : new File([input.audio], filename, { type: input.mimeType || input.audio.type });
400
+ ? new File([Uint8Array.from(input.audio)], filename, {
401
+ type: input.mimeType,
402
+ })
403
+ : new File([input.audio], filename, {
404
+ type: input.mimeType || input.audio.type,
405
+ });
365
406
  form.append("audio", audio, filename);
366
407
  form.append("mimeType", input.mimeType);
367
408
  if (input.durationSeconds !== undefined) {
@@ -371,7 +412,10 @@ export class OpenGeniClient {
371
412
  try {
372
413
  response = await this.fetchImpl(this.url(`/v1/workspaces/${workspaceId}/transcriptions`), {
373
414
  method: "POST",
374
- headers: { ...this.headers(correlationId), Accept: "application/json" },
415
+ headers: {
416
+ ...this.headers(correlationId),
417
+ Accept: "application/json",
418
+ },
375
419
  body: form,
376
420
  ...(input.signal ? { signal: input.signal } : {}),
377
421
  });
@@ -380,7 +424,11 @@ export class OpenGeniClient {
380
424
  throw mutationTransportError(correlationId);
381
425
  }
382
426
  assertApiContractResponse(response);
383
- if (!response.ok) throw await apiErrorFromResponse(response, { method: "POST", correlationId });
427
+ if (!response.ok)
428
+ throw await apiErrorFromResponse(response, {
429
+ method: "POST",
430
+ correlationId,
431
+ });
384
432
  await assertJsonResponse(response, { method: "POST", correlationId });
385
433
  let body: unknown;
386
434
  try {
@@ -402,6 +450,157 @@ export class OpenGeniClient {
402
450
  return body;
403
451
  }
404
452
 
453
+ async createTranscriptionRecording(
454
+ workspaceId: string,
455
+ input: CreateTranscriptionRecordingInput,
456
+ ): Promise<TranscriptionRecordingResponse> {
457
+ return transcriptionRecordingResponse(
458
+ await this.requestJson<unknown>(
459
+ "POST",
460
+ `/v1/workspaces/${workspaceId}/transcription-recordings`,
461
+ { recordingId: input.recordingId, mimeType: input.mimeType },
462
+ {},
463
+ input.signal ? { signal: input.signal } : {},
464
+ ),
465
+ );
466
+ }
467
+
468
+ async getTranscriptionRecording(
469
+ workspaceId: string,
470
+ recordingId: string,
471
+ options: { signal?: AbortSignal | undefined } = {},
472
+ ): Promise<TranscriptionRecordingResponse> {
473
+ return transcriptionRecordingResponse(
474
+ await this.requestJson<unknown>(
475
+ "GET",
476
+ `/v1/workspaces/${workspaceId}/transcription-recordings/${recordingId}`,
477
+ undefined,
478
+ {},
479
+ options.signal ? { signal: options.signal } : {},
480
+ ),
481
+ );
482
+ }
483
+
484
+ async listTranscriptionRecordings(
485
+ workspaceId: string,
486
+ options: { signal?: AbortSignal | undefined } = {},
487
+ ): Promise<TranscriptionRecordingListResponse> {
488
+ const body = await this.requestJson<unknown>(
489
+ "GET",
490
+ `/v1/workspaces/${workspaceId}/transcription-recordings`,
491
+ undefined,
492
+ {},
493
+ options.signal ? { signal: options.signal } : {},
494
+ );
495
+ if (!isTranscriptionRecordingListResponse(body)) {
496
+ throw new OpenGeniApiError(502, "Invalid transcription recording list response.", {
497
+ code: "invalid_response",
498
+ });
499
+ }
500
+ return body;
501
+ }
502
+
503
+ async uploadTranscriptionRecordingChunk(
504
+ workspaceId: string,
505
+ recordingId: string,
506
+ chunkNumber: number,
507
+ input: UploadTranscriptionRecordingChunkInput,
508
+ ): Promise<UploadTranscriptionRecordingChunkResponse> {
509
+ const correlationId = crypto.randomUUID();
510
+ let response: Response;
511
+ try {
512
+ response = await this.fetchImpl(
513
+ this.url(
514
+ `/v1/workspaces/${workspaceId}/transcription-recordings/${recordingId}/chunks/${chunkNumber}`,
515
+ ),
516
+ {
517
+ method: "PUT",
518
+ headers: {
519
+ ...this.headers(correlationId),
520
+ Accept: "application/json",
521
+ "Content-Type": input.mimeType,
522
+ "x-opengeni-chunk-sha256": input.sha256,
523
+ "x-opengeni-chunk-start-milliseconds": String(input.startMilliseconds),
524
+ "x-opengeni-chunk-duration-milliseconds": String(input.durationMilliseconds),
525
+ },
526
+ body: input.audio instanceof Uint8Array ? Uint8Array.from(input.audio) : input.audio,
527
+ ...(input.signal ? { signal: input.signal } : {}),
528
+ },
529
+ );
530
+ } catch (error) {
531
+ if (input.signal?.aborted) throw error;
532
+ throw mutationTransportError(correlationId);
533
+ }
534
+ assertApiContractResponse(response);
535
+ if (!response.ok)
536
+ throw await apiErrorFromResponse(response, {
537
+ method: "PUT",
538
+ correlationId,
539
+ });
540
+ await assertJsonResponse(response, { method: "PUT", correlationId });
541
+ const body = await response.json().catch(() => null);
542
+ if (!isUploadTranscriptionRecordingChunkResponse(body)) {
543
+ throw new OpenGeniApiError(response.status, "Invalid transcription chunk response.", {
544
+ code: "invalid_response",
545
+ mutation: true,
546
+ correlationId,
547
+ });
548
+ }
549
+ return body;
550
+ }
551
+
552
+ async finalizeTranscriptionRecording(
553
+ workspaceId: string,
554
+ recordingId: string,
555
+ input: FinalizeTranscriptionRecordingInput,
556
+ ): Promise<TranscriptionRecordingResponse> {
557
+ return transcriptionRecordingResponse(
558
+ await this.requestJson<unknown>(
559
+ "POST",
560
+ `/v1/workspaces/${workspaceId}/transcription-recordings/${recordingId}/finalize`,
561
+ {
562
+ chunkCount: input.chunkCount,
563
+ totalBytes: input.totalBytes,
564
+ totalDurationMilliseconds: input.totalDurationMilliseconds,
565
+ },
566
+ {},
567
+ input.signal ? { signal: input.signal } : {},
568
+ ),
569
+ );
570
+ }
571
+
572
+ async processNextTranscriptionRecordingSegment(
573
+ workspaceId: string,
574
+ recordingId: string,
575
+ options: { signal?: AbortSignal | undefined } = {},
576
+ ): Promise<TranscriptionRecordingResponse> {
577
+ return transcriptionRecordingResponse(
578
+ await this.requestJson<unknown>(
579
+ "POST",
580
+ `/v1/workspaces/${workspaceId}/transcription-recordings/${recordingId}/process-next`,
581
+ {},
582
+ {},
583
+ options.signal ? { signal: options.signal } : {},
584
+ ),
585
+ );
586
+ }
587
+
588
+ async discardTranscriptionRecording(
589
+ workspaceId: string,
590
+ recordingId: string,
591
+ options: { signal?: AbortSignal | undefined } = {},
592
+ ): Promise<TranscriptionRecordingResponse> {
593
+ return transcriptionRecordingResponse(
594
+ await this.requestJson<unknown>(
595
+ "DELETE",
596
+ `/v1/workspaces/${workspaceId}/transcription-recordings/${recordingId}`,
597
+ undefined,
598
+ {},
599
+ options.signal ? { signal: options.signal } : {},
600
+ ),
601
+ );
602
+ }
603
+
405
604
  async createSession(
406
605
  workspaceId: string,
407
606
  request: CreateSessionRequest,
@@ -431,11 +630,13 @@ export class OpenGeniClient {
431
630
  );
432
631
  }
433
632
 
434
- async getSession(workspaceId: string, sessionId: string): Promise<Session> {
435
- return await this.requestJson<Session>(
436
- "GET",
437
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}`,
438
- );
633
+ async getSession(
634
+ workspaceId: string,
635
+ sessionId: string,
636
+ options: { fresh?: boolean } = {},
637
+ ): Promise<Session> {
638
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}`;
639
+ return await this.singleFlightRead(path, () => this.requestJson<Session>("GET", path), options);
439
640
  }
440
641
 
441
642
  async updateSession(
@@ -582,12 +783,40 @@ export class OpenGeniClient {
582
783
  }
583
784
 
584
785
  async getSessionLineage(workspaceId: string, sessionId: string): Promise<SessionLineageResponse> {
585
- return await this.requestJson<SessionLineageResponse>(
586
- "GET",
587
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/lineage`,
786
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}/lineage`;
787
+ return await this.singleFlightRead(path, () =>
788
+ this.requestJson<SessionLineageResponse>("GET", path),
588
789
  );
589
790
  }
590
791
 
792
+ private singleFlightRead<T>(
793
+ key: string,
794
+ read: () => Promise<T>,
795
+ options: { fresh?: boolean } = {},
796
+ ): Promise<T> {
797
+ const existing = this.readInFlight.get(key);
798
+ if (existing) {
799
+ if (!options.fresh) return existing as Promise<T>;
800
+ const queued = this.readTrailing.get(key);
801
+ if (queued) return queued as Promise<T>;
802
+ const trailing = existing.then(
803
+ () => this.singleFlightRead(key, read),
804
+ () => this.singleFlightRead(key, read),
805
+ );
806
+ this.readTrailing.set(key, trailing);
807
+ const clear = () => {
808
+ if (this.readTrailing.get(key) === trailing) this.readTrailing.delete(key);
809
+ };
810
+ void trailing.then(clear, clear);
811
+ return trailing;
812
+ }
813
+ const promise = read().finally(() => {
814
+ if (this.readInFlight.get(key) === promise) this.readInFlight.delete(key);
815
+ });
816
+ this.readInFlight.set(key, promise);
817
+ return promise;
818
+ }
819
+
591
820
  /** Negotiate one server-mediated connected-Codex GPT-Live V3 WebRTC call. */
592
821
  async negotiateCodexRealtimeWebrtc(
593
822
  workspaceId: string,
@@ -711,7 +940,9 @@ export class OpenGeniClient {
711
940
 
712
941
  /** Newest turn that durably emitted `turn.started`, or null before any admission. */
713
942
  async getLatestStartedTurn(workspaceId: string, sessionId: string): Promise<SessionTurn | null> {
714
- const turns = await this.listTurns(workspaceId, sessionId, { latestStarted: true });
943
+ const turns = await this.listTurns(workspaceId, sessionId, {
944
+ latestStarted: true,
945
+ });
715
946
  return turns[0] ?? null;
716
947
  }
717
948
 
@@ -756,6 +987,24 @@ export class OpenGeniClient {
756
987
  return response.samples;
757
988
  }
758
989
 
990
+ /**
991
+ * Remove one connected self-hosted machine enrollment. The control-plane
992
+ * operation works while the agent is offline, revokes future reconnects,
993
+ * retains history, and returns a typed blocker when active route/lease or
994
+ * recovery dependencies make removal unsafe. `idempotencyKey` is replay-safe.
995
+ */
996
+ async removeEnrollment(
997
+ workspaceId: string,
998
+ enrollmentId: string,
999
+ request: RemoveEnrollmentRequest = {},
1000
+ ): Promise<RemoveEnrollmentResponse> {
1001
+ return await this.requestJson<RemoveEnrollmentResponse>(
1002
+ "POST",
1003
+ `/v1/workspaces/${workspaceId}/enrollments/${enrollmentId}/revoke`,
1004
+ request,
1005
+ );
1006
+ }
1007
+
759
1008
  // --- Self-hosted enrollment UX (design 11) --------------------------------
760
1009
 
761
1010
  /**
@@ -1178,9 +1427,9 @@ export class OpenGeniClient {
1178
1427
  // --- Turn queue ------------------------------------------------------------
1179
1428
 
1180
1429
  async getQueue(workspaceId: string, sessionId: string): Promise<SessionQueueSnapshot> {
1181
- return await this.requestJson<SessionQueueSnapshot>(
1182
- "GET",
1183
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue`,
1430
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue`;
1431
+ return await this.singleFlightRead(path, () =>
1432
+ this.requestJson<SessionQueueSnapshot>("GET", path),
1184
1433
  );
1185
1434
  }
1186
1435
 
@@ -1297,7 +1546,11 @@ export class OpenGeniClient {
1297
1546
  async cancelSession(
1298
1547
  workspaceId: string,
1299
1548
  sessionId: string,
1300
- options: { reason?: string; clientEventId?: string; expectedControlEtag?: string } = {},
1549
+ options: {
1550
+ reason?: string;
1551
+ clientEventId?: string;
1552
+ expectedControlEtag?: string;
1553
+ } = {},
1301
1554
  ): Promise<SessionControlResponse> {
1302
1555
  return await this.controlSession(workspaceId, sessionId, {
1303
1556
  action: "cancel",
@@ -1442,10 +1695,8 @@ export class OpenGeniClient {
1442
1695
 
1443
1696
  /** The session's goal. 404s when the session never had one. */
1444
1697
  async getGoal(workspaceId: string, sessionId: string): Promise<SessionGoal> {
1445
- return await this.requestJson<SessionGoal>(
1446
- "GET",
1447
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`,
1448
- );
1698
+ const path = `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`;
1699
+ return await this.singleFlightRead(path, () => this.requestJson<SessionGoal>("GET", path));
1449
1700
  }
1450
1701
 
1451
1702
  async updateGoal(
@@ -1531,6 +1782,22 @@ export class OpenGeniClient {
1531
1782
  );
1532
1783
  }
1533
1784
 
1785
+ /** FileSystem: hydrate several independent directories behind one sandbox lease. */
1786
+ async fsListBatch(
1787
+ workspaceId: string,
1788
+ sessionId: string,
1789
+ request: FsListBatchRequest,
1790
+ options: OpenGeniRequestOptions = {},
1791
+ ): Promise<FsListBatchResponse> {
1792
+ return await this.requestJson<FsListBatchResponse>(
1793
+ "POST",
1794
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/list-batch`,
1795
+ request,
1796
+ {},
1797
+ options,
1798
+ );
1799
+ }
1800
+
1534
1801
  /** FileSystem: read a file (text or base64; binary-safe, size-capped). */
1535
1802
  async fsRead(
1536
1803
  workspaceId: string,
@@ -1631,6 +1898,22 @@ export class OpenGeniClient {
1631
1898
  );
1632
1899
  }
1633
1900
 
1901
+ /** Git: read status and optional diffs for several repositories behind one sandbox lease. */
1902
+ async gitReadBatch(
1903
+ workspaceId: string,
1904
+ sessionId: string,
1905
+ request: GitReadBatchRequest,
1906
+ options: OpenGeniRequestOptions = {},
1907
+ ): Promise<GitReadBatchResponse> {
1908
+ return await this.requestJson<GitReadBatchResponse>(
1909
+ "POST",
1910
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/read-batch`,
1911
+ request,
1912
+ {},
1913
+ options,
1914
+ );
1915
+ }
1916
+
1634
1917
  /** Git: commit log. */
1635
1918
  async gitLog(
1636
1919
  workspaceId: string,
@@ -2312,7 +2595,8 @@ export class OpenGeniClient {
2312
2595
  }
2313
2596
 
2314
2597
  // --- VariableSets --------------------------------------------------------------
2315
- // Variable values are write-only: reads return name/version metadata only.
2598
+ // Generic reads return name/version metadata only. Plaintext uses one
2599
+ // dedicated permissioned endpoint.
2316
2600
 
2317
2601
  async listVariableSets(workspaceId: string): Promise<VariableSet[]> {
2318
2602
  return await this.requestJson<VariableSet[]>(
@@ -2339,6 +2623,17 @@ export class OpenGeniClient {
2339
2623
  );
2340
2624
  }
2341
2625
 
2626
+ async getVariableSetVariable(
2627
+ workspaceId: string,
2628
+ variableSetId: string,
2629
+ name: string,
2630
+ ): Promise<VariableSetSecret> {
2631
+ return await this.requestJson<VariableSetSecret>(
2632
+ "GET",
2633
+ `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}/variables/${encodeURIComponent(name)}`,
2634
+ );
2635
+ }
2636
+
2342
2637
  async updateVariableSet(
2343
2638
  workspaceId: string,
2344
2639
  variableSetId: string,
@@ -2358,7 +2653,7 @@ export class OpenGeniClient {
2358
2653
  );
2359
2654
  }
2360
2655
 
2361
- /** Create or rotate a variable. The value never comes back on any read. */
2656
+ /** Create or rotate a variable. Generic reads never return its value. */
2362
2657
  async setVariableSetVariable(
2363
2658
  workspaceId: string,
2364
2659
  variableSetId: string,
@@ -2554,19 +2849,29 @@ export class OpenGeniClient {
2554
2849
  async beginFileUpload(
2555
2850
  workspaceId: string,
2556
2851
  request: CreateFileUploadRequest,
2852
+ options: OpenGeniRequestOptions = {},
2557
2853
  ): Promise<CreateFileUploadResponse> {
2558
2854
  return await this.requestJson<CreateFileUploadResponse>(
2559
2855
  "POST",
2560
2856
  `/v1/workspaces/${workspaceId}/files/uploads`,
2561
2857
  request,
2858
+ {},
2859
+ options,
2562
2860
  );
2563
2861
  }
2564
2862
 
2565
2863
  /** Step 3 of the upload flow: server verifies the object and marks it ready. */
2566
- async completeFileUpload(workspaceId: string, uploadId: string): Promise<FileAsset> {
2864
+ async completeFileUpload(
2865
+ workspaceId: string,
2866
+ uploadId: string,
2867
+ options: OpenGeniRequestOptions = {},
2868
+ ): Promise<FileAsset> {
2567
2869
  const response = await this.requestJson<CompleteFileUploadResponse>(
2568
2870
  "POST",
2569
2871
  `/v1/workspaces/${workspaceId}/files/uploads/${uploadId}/complete`,
2872
+ undefined,
2873
+ {},
2874
+ options,
2570
2875
  );
2571
2876
  return response.file;
2572
2877
  }
@@ -2577,6 +2882,30 @@ export class OpenGeniClient {
2577
2882
  * -> complete. Returns the ready `FileAsset`.
2578
2883
  */
2579
2884
  async uploadFile(workspaceId: string, input: UploadFileInput): Promise<FileAsset> {
2885
+ if (
2886
+ input.timeoutMs !== undefined &&
2887
+ (!Number.isFinite(input.timeoutMs) || input.timeoutMs <= 0)
2888
+ ) {
2889
+ throw new Error("File upload timeout must be a positive number");
2890
+ }
2891
+ const withTimeout = async <T>(
2892
+ timeoutMs: number,
2893
+ operation: (signal: AbortSignal) => Promise<T>,
2894
+ ): Promise<T> => {
2895
+ const controller = new AbortController();
2896
+ let timeout: ReturnType<typeof setTimeout> | undefined;
2897
+ const timedOut = new Promise<never>((_resolve, reject) => {
2898
+ timeout = setTimeout(() => {
2899
+ controller.abort();
2900
+ reject(new Error("File upload timed out. Retry the upload."));
2901
+ }, timeoutMs);
2902
+ });
2903
+ try {
2904
+ return await Promise.race([operation(controller.signal), timedOut]);
2905
+ } finally {
2906
+ if (timeout !== undefined) clearTimeout(timeout);
2907
+ }
2908
+ };
2580
2909
  // Snapshot mutable inputs before hashing so the digest always describes the
2581
2910
  // exact bytes later sent to object storage. Copy Uint8Array views into a
2582
2911
  // Blob so byte offsets/shared buffers can't leak surrounding bytes.
@@ -2593,32 +2922,52 @@ export class OpenGeniClient {
2593
2922
  ? body.size
2594
2923
  : body.byteLength;
2595
2924
  const sha256 = input.sha256 ?? (await sha256ForUpload(body));
2596
- const upload = await this.beginFileUpload(workspaceId, {
2597
- filename: input.filename,
2598
- contentType: input.contentType,
2599
- sizeBytes,
2600
- sha256,
2601
- });
2602
- const putResponse = await this.fetchImpl(upload.putUrl, {
2603
- method: "PUT",
2604
- // Signed object-storage URLs carry their own short-lived authority.
2605
- // Browser cookies and HTTP auth must never accompany this cross-origin
2606
- // request: credentialed fetches are incompatible with wildcard CORS and
2607
- // can leak ambient credentials to a caller-selected storage endpoint.
2608
- credentials: "omit",
2609
- // The backend's requiredHeaders already carry the canonical lowercase
2610
- // `content-type` for every storage backend (Azure/S3/GCS). Do NOT also set
2611
- // a `Content-Type` key here: WHATWG Headers treats the two casings as the
2612
- // same header and comma-joins their values (e.g. "text/plain, text/plain"),
2613
- // which the object store persists verbatim and COMPLETE then rejects (422),
2614
- // and which breaks S3's presigned-URL signature.
2615
- headers: { ...upload.requiredHeaders },
2616
- body,
2617
- });
2925
+ const upload = await withTimeout(
2926
+ 30_000,
2927
+ async (signal) =>
2928
+ await this.beginFileUpload(
2929
+ workspaceId,
2930
+ {
2931
+ filename: input.filename,
2932
+ contentType: input.contentType,
2933
+ sizeBytes,
2934
+ sha256,
2935
+ },
2936
+ { signal },
2937
+ ),
2938
+ );
2939
+ // Give large valid uploads enough time at a conservative 256 KiB/s while
2940
+ // still bounding an object-storage request that never settles.
2941
+ const transferTimeoutMs =
2942
+ input.timeoutMs ?? Math.max(120_000, Math.ceil(sizeBytes / (256 * 1024)) * 1_000);
2943
+ const putResponse = await withTimeout(
2944
+ transferTimeoutMs,
2945
+ async (signal) =>
2946
+ await this.fetchImpl(upload.putUrl, {
2947
+ method: "PUT",
2948
+ // Signed object-storage URLs carry their own short-lived authority.
2949
+ // Browser cookies and HTTP auth must never accompany this cross-origin
2950
+ // request: credentialed fetches are incompatible with wildcard CORS and
2951
+ // can leak ambient credentials to a caller-selected storage endpoint.
2952
+ credentials: "omit",
2953
+ // The backend's requiredHeaders already carry the canonical lowercase
2954
+ // `content-type` for every storage backend (Azure/S3/GCS). Do NOT also set
2955
+ // a `Content-Type` key here: WHATWG Headers treats the two casings as the
2956
+ // same header and comma-joins their values (e.g. "text/plain, text/plain"),
2957
+ // which the object store persists verbatim and COMPLETE then rejects (422),
2958
+ // and which breaks S3's presigned-URL signature.
2959
+ headers: { ...upload.requiredHeaders },
2960
+ body,
2961
+ signal,
2962
+ }),
2963
+ );
2618
2964
  if (!putResponse.ok) {
2619
2965
  throw await apiErrorFromResponse(putResponse, { method: "PUT" });
2620
2966
  }
2621
- return await this.completeFileUpload(workspaceId, upload.uploadId);
2967
+ return await withTimeout(
2968
+ 30_000,
2969
+ async (signal) => await this.completeFileUpload(workspaceId, upload.uploadId, { signal }),
2970
+ );
2622
2971
  }
2623
2972
 
2624
2973
  async getFile(workspaceId: string, fileId: string): Promise<FileAsset> {
@@ -2647,23 +2996,119 @@ export class OpenGeniClient {
2647
2996
  workspaceId: string,
2648
2997
  artifactId: string,
2649
2998
  options: RetainedArtifactContentOptions = {},
2999
+ ): Promise<RetainedArtifactContent> {
3000
+ return await this.getRetainedArtifactContentAtPath(
3001
+ `/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`,
3002
+ options,
3003
+ );
3004
+ }
3005
+
3006
+ async getSessionRetainedArtifact(
3007
+ workspaceId: string,
3008
+ sessionId: string,
3009
+ artifactId: string,
3010
+ ): Promise<RetainedArtifactMetadata> {
3011
+ return await this.requestJson<RetainedArtifactMetadata>(
3012
+ "GET",
3013
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/artifacts/${artifactId}`,
3014
+ );
3015
+ }
3016
+
3017
+ async getSessionRetainedArtifactContent(
3018
+ workspaceId: string,
3019
+ sessionId: string,
3020
+ artifactId: string,
3021
+ options: RetainedArtifactContentOptions = {},
3022
+ ): Promise<RetainedArtifactContent> {
3023
+ return await this.getRetainedArtifactContentAtPath(
3024
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/artifacts/${artifactId}/content`,
3025
+ options,
3026
+ );
3027
+ }
3028
+
3029
+ /** Assemble one retained screenshot from bounded authenticated API ranges. */
3030
+ async downloadRetainedScreenshot(
3031
+ workspaceId: string,
3032
+ sessionId: string,
3033
+ artifactId: string,
3034
+ options: RetainedScreenshotDownloadOptions = {},
3035
+ ): Promise<RetainedScreenshotDownload> {
3036
+ const maxRetries = options.maxRetries ?? 2;
3037
+ if (!Number.isInteger(maxRetries) || maxRetries < 0 || maxRetries > 3) {
3038
+ throw new RangeError("retained screenshot maxRetries must be an integer from 0 to 3");
3039
+ }
3040
+ const metadata = await this.getSessionRetainedArtifact(workspaceId, sessionId, artifactId);
3041
+ if (!metadata.available) return { metadata, bytes: null };
3042
+ if (
3043
+ metadata.kind !== "computer_screenshot" ||
3044
+ metadata.contentType !== "image/png" ||
3045
+ !metadata.dimensions ||
3046
+ metadata.originalBytes <= 0 ||
3047
+ metadata.originalBytes > COMPUTER_SCREENSHOT_MAX_BYTES
3048
+ ) {
3049
+ throw new OpenGeniApiError(502, "retained screenshot metadata is invalid");
3050
+ }
3051
+ const bytes = new Uint8Array(metadata.originalBytes);
3052
+ const pageBytes = Math.min(metadata.retrieval.maxRangeBytes, RETAINED_OUTPUT_MAX_PAGE_BYTES);
3053
+ if (!Number.isSafeInteger(pageBytes) || pageBytes <= 0) {
3054
+ throw new OpenGeniApiError(502, "retained screenshot range metadata is invalid");
3055
+ }
3056
+ for (let start = 0; start < bytes.byteLength; start += pageBytes) {
3057
+ options.signal?.throwIfAborted();
3058
+ const end = Math.min(start + pageBytes, bytes.byteLength) - 1;
3059
+ let page: RetainedArtifactContent | null = null;
3060
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
3061
+ try {
3062
+ page = await this.getSessionRetainedArtifactContent(workspaceId, sessionId, artifactId, {
3063
+ range: `bytes=${start}-${end}`,
3064
+ ...(options.signal ? { signal: options.signal } : {}),
3065
+ });
3066
+ break;
3067
+ } catch (error) {
3068
+ options.signal?.throwIfAborted();
3069
+ if (
3070
+ attempt >= maxRetries ||
3071
+ (error instanceof OpenGeniApiError && error.status >= 400 && error.status < 500)
3072
+ ) {
3073
+ throw error;
3074
+ }
3075
+ }
3076
+ }
3077
+ if (!page) throw new OpenGeniApiError(502, "retained screenshot range retry exhausted");
3078
+ const expectedLength = end - start + 1;
3079
+ if (
3080
+ page.status !== 206 ||
3081
+ page.contentType !== metadata.contentType ||
3082
+ page.contentLength !== expectedLength ||
3083
+ page.contentRange !== `bytes ${start}-${end}/${metadata.originalBytes}`
3084
+ ) {
3085
+ throw new OpenGeniApiError(502, "retained screenshot range response is invalid");
3086
+ }
3087
+ bytes.set(page.bytes, start);
3088
+ }
3089
+ if ((await sha256Hex(bytes)) !== metadata.sha256) {
3090
+ throw new OpenGeniApiError(502, "retained screenshot checksum mismatch");
3091
+ }
3092
+ return { metadata, bytes };
3093
+ }
3094
+
3095
+ private async getRetainedArtifactContentAtPath(
3096
+ path: string,
3097
+ options: RetainedArtifactContentOptions,
2650
3098
  ): Promise<RetainedArtifactContent> {
2651
3099
  if (options.range && (options.range.length > 128 || /[^\x20-\x7e]/.test(options.range))) {
2652
3100
  throw new RangeError("retained artifact range must be at most 128 printable ASCII bytes");
2653
3101
  }
2654
3102
  const correlationId = crypto.randomUUID();
2655
- const response = await this.fetchImpl(
2656
- this.url(`/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`),
2657
- {
2658
- method: "GET",
2659
- headers: {
2660
- ...this.headers(correlationId),
2661
- Accept: "application/octet-stream",
2662
- ...(options.range ? { Range: options.range } : {}),
2663
- },
2664
- ...(options.signal ? { signal: options.signal } : {}),
3103
+ const response = await this.fetchImpl(this.url(path), {
3104
+ method: "GET",
3105
+ headers: {
3106
+ ...this.headers(correlationId),
3107
+ Accept: "application/octet-stream",
3108
+ ...(options.range ? { Range: options.range } : {}),
2665
3109
  },
2666
- );
3110
+ ...(options.signal ? { signal: options.signal } : {}),
3111
+ });
2667
3112
  try {
2668
3113
  assertApiContractResponse(response);
2669
3114
  } catch (error) {
@@ -3595,6 +4040,91 @@ function isTranscribeAudioResponse(value: unknown): value is TranscribeAudioResp
3595
4040
  );
3596
4041
  }
3597
4042
 
4043
+ function isUploadTranscriptionRecordingChunkResponse(
4044
+ value: unknown,
4045
+ ): value is UploadTranscriptionRecordingChunkResponse {
4046
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4047
+ const record = value as Record<string, unknown>;
4048
+ if (
4049
+ !isTranscriptionRecordingResponse({
4050
+ recording: record.recording,
4051
+ segments: [],
4052
+ })
4053
+ ) {
4054
+ return false;
4055
+ }
4056
+ if (!record.chunk || typeof record.chunk !== "object" || Array.isArray(record.chunk)) {
4057
+ return false;
4058
+ }
4059
+ const chunk = record.chunk as Record<string, unknown>;
4060
+ return (
4061
+ typeof chunk.chunkNumber === "number" &&
4062
+ typeof chunk.byteLength === "number" &&
4063
+ typeof chunk.sha256 === "string" &&
4064
+ typeof chunk.startMilliseconds === "number" &&
4065
+ typeof chunk.durationMilliseconds === "number" &&
4066
+ typeof chunk.deduplicated === "boolean"
4067
+ );
4068
+ }
4069
+
4070
+ function isTranscriptionRecordingResponse(value: unknown): value is TranscriptionRecordingResponse {
4071
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4072
+ const response = value as Record<string, unknown>;
4073
+ if (
4074
+ !response.recording ||
4075
+ typeof response.recording !== "object" ||
4076
+ Array.isArray(response.recording)
4077
+ ) {
4078
+ return false;
4079
+ }
4080
+ const recording = response.recording as Record<string, unknown>;
4081
+ return (
4082
+ typeof recording.id === "string" &&
4083
+ typeof recording.workspaceId === "string" &&
4084
+ typeof recording.mimeType === "string" &&
4085
+ typeof recording.state === "string" &&
4086
+ typeof recording.nextChunkNumber === "number" &&
4087
+ typeof recording.chunkCount === "number" &&
4088
+ typeof recording.totalBytes === "number" &&
4089
+ typeof recording.totalDurationMilliseconds === "number" &&
4090
+ typeof recording.segmentCount === "number" &&
4091
+ typeof recording.completedSegmentCount === "number" &&
4092
+ (recording.transcriptText === null || typeof recording.transcriptText === "string") &&
4093
+ Array.isArray(recording.languages) &&
4094
+ (recording.errorCode === null || typeof recording.errorCode === "string") &&
4095
+ typeof recording.retryable === "boolean" &&
4096
+ typeof recording.objectsCleaned === "boolean" &&
4097
+ typeof recording.createdAt === "string" &&
4098
+ typeof recording.updatedAt === "string" &&
4099
+ typeof recording.expiresAt === "string" &&
4100
+ (response.retryAfterMilliseconds === undefined ||
4101
+ (typeof response.retryAfterMilliseconds === "number" &&
4102
+ Number.isInteger(response.retryAfterMilliseconds) &&
4103
+ response.retryAfterMilliseconds > 0 &&
4104
+ response.retryAfterMilliseconds <= 60_000)) &&
4105
+ Array.isArray(response.segments)
4106
+ );
4107
+ }
4108
+
4109
+ function transcriptionRecordingResponse(value: unknown): TranscriptionRecordingResponse {
4110
+ if (isTranscriptionRecordingResponse(value)) return value;
4111
+ throw new OpenGeniApiError(502, "Invalid transcription recording response.", {
4112
+ code: "invalid_response",
4113
+ });
4114
+ }
4115
+
4116
+ function isTranscriptionRecordingListResponse(
4117
+ value: unknown,
4118
+ ): value is TranscriptionRecordingListResponse {
4119
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4120
+ const recordings = (value as Record<string, unknown>).recordings;
4121
+ return (
4122
+ Array.isArray(recordings) &&
4123
+ recordings.length <= 50 &&
4124
+ recordings.every((recording) => isTranscriptionRecordingResponse({ recording, segments: [] }))
4125
+ );
4126
+ }
4127
+
3598
4128
  function filenameForAudioMimeType(mimeType: string): string {
3599
4129
  const bare = mimeType.trim().toLowerCase().split(";")[0] ?? "audio/webm";
3600
4130
  switch (bare) {
@@ -3695,6 +4225,12 @@ async function sha256ForUpload(body: Blob | ArrayBuffer | string): Promise<strin
3695
4225
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
3696
4226
  }
3697
4227
 
4228
+ async function sha256Hex(bytes: Uint8Array): Promise<string> {
4229
+ const owned = Uint8Array.from(bytes);
4230
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", owned.buffer);
4231
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4232
+ }
4233
+
3698
4234
  async function cancelResponseBody(response: Response, reason: string): Promise<void> {
3699
4235
  await response.body?.cancel(reason).catch(() => undefined);
3700
4236
  }