@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/src/stream.ts CHANGED
@@ -29,13 +29,25 @@ export type StreamSessionEventsOptions = {
29
29
  reconnectDelayMs?: number;
30
30
  /** Backoff ceiling. Defaults to 10s. */
31
31
  maxReconnectDelayMs?: number;
32
+ /** Random backoff spread as a fraction of the delay. Defaults to 0.2. */
33
+ reconnectJitterRatio?: number;
32
34
  /**
33
35
  * Give up after this many consecutive failed reconnect attempts (i.e. N
34
36
  * reconnects = N+1 total open-stream calls). Defaults to unlimited.
35
37
  */
36
38
  maxReconnectAttempts?: number;
39
+ /**
40
+ * Notify the consumer as soon as the SSE response body is open. This callback
41
+ * is deliberately synchronous and non-blocking: use it to start projection
42
+ * reconciliation without delaying consumption of live event bytes.
43
+ */
44
+ onOpen?: (() => void) | undefined;
37
45
  /** Await authoritative client reconciliation before exposing `live`. */
38
46
  beforeLive?: (() => void | Promise<void>) | undefined;
47
+ /** Maximum reconciliation time before reconnecting. Defaults to 15s. */
48
+ beforeLiveTimeoutMs?: number;
49
+ /** Maximum time without any SSE bytes before reconnecting. Defaults to 45s. */
50
+ heartbeatTimeoutMs?: number;
39
51
  onStateChange?: (state: StreamConnectionState) => void;
40
52
  };
41
53
 
@@ -62,6 +74,9 @@ export async function* streamSessionEvents(
62
74
  const reconnect = options.reconnect ?? true;
63
75
  const baseDelayMs = options.reconnectDelayMs ?? 500;
64
76
  const maxDelayMs = options.maxReconnectDelayMs ?? 10_000;
77
+ const jitterRatio = options.reconnectJitterRatio ?? 0.2;
78
+ const beforeLiveTimeoutMs = options.beforeLiveTimeoutMs ?? 15_000;
79
+ const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? 45_000;
65
80
  const maxAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
66
81
  let cursor = options.after ?? 0;
67
82
  let failedAttempts = 0;
@@ -77,9 +92,16 @@ export async function* streamSessionEvents(
77
92
  everConnected = true;
78
93
  failedAttempts = 0;
79
94
  delayMs = baseDelayMs;
80
- await options.beforeLive?.();
95
+ try {
96
+ options.onOpen?.();
97
+ } catch {
98
+ // A notification observer cannot own or interrupt the event transport.
99
+ }
100
+ await runBeforeLive(options.beforeLive, beforeLiveTimeoutMs, signal);
81
101
  options.onStateChange?.("live");
82
- for await (const message of parseSseStream(body)) {
102
+ for await (const message of parseSseStream(
103
+ withStreamInactivityTimeout(body, heartbeatTimeoutMs, signal),
104
+ )) {
83
105
  // Re-check after every yield resumption: an abort from the consumer
84
106
  // must not let already-buffered events keep flowing.
85
107
  if (signal?.aborted) {
@@ -108,7 +130,7 @@ export async function* streamSessionEvents(
108
130
  // progress (servers legitimately cycle long SSE connections); pace
109
131
  // empty closes so a misbehaving server is not hammered in a hot loop.
110
132
  if (cursor === cursorAtOpen) {
111
- await sleep(baseDelayMs, signal);
133
+ await sleep(jitteredDelay(baseDelayMs, jitterRatio), signal);
112
134
  }
113
135
  continue;
114
136
  } catch (error) {
@@ -125,11 +147,87 @@ export async function* streamSessionEvents(
125
147
  );
126
148
  }
127
149
  }
128
- await sleep(delayMs, signal);
150
+ await sleep(jitteredDelay(delayMs, jitterRatio), signal);
129
151
  delayMs = Math.min(Math.max(delayMs * 2, baseDelayMs), maxDelayMs);
130
152
  }
131
153
  }
132
154
 
155
+ export async function runBeforeLive(
156
+ beforeLive: (() => void | Promise<void>) | undefined,
157
+ timeoutMs: number,
158
+ signal: AbortSignal | undefined,
159
+ ): Promise<void> {
160
+ if (!beforeLive) return;
161
+ let timer: ReturnType<typeof setTimeout> | undefined;
162
+ let abortListener: (() => void) | undefined;
163
+ const timeout = new Promise<never>((_resolve, reject) => {
164
+ timer = setTimeout(
165
+ () => reject(new TypeError(`stream reconciliation timed out after ${timeoutMs}ms`)),
166
+ timeoutMs,
167
+ );
168
+ if (signal) {
169
+ abortListener = () => reject(new DOMException("Aborted", "AbortError"));
170
+ signal.addEventListener("abort", abortListener, { once: true });
171
+ }
172
+ });
173
+ try {
174
+ await Promise.race([Promise.resolve().then(beforeLive), timeout]);
175
+ } finally {
176
+ if (timer !== undefined) clearTimeout(timer);
177
+ if (signal && abortListener) signal.removeEventListener("abort", abortListener);
178
+ }
179
+ }
180
+
181
+ export function jitteredDelay(delayMs: number, ratio: number): number {
182
+ if (delayMs <= 0 || ratio <= 0) return delayMs;
183
+ const boundedRatio = Math.min(ratio, 1);
184
+ const spread = delayMs * boundedRatio;
185
+ return Math.max(0, delayMs - spread + Math.random() * spread * 2);
186
+ }
187
+
188
+ export function withStreamInactivityTimeout(
189
+ stream: ReadableStream<Uint8Array>,
190
+ timeoutMs: number,
191
+ signal: AbortSignal | undefined,
192
+ ): ReadableStream<Uint8Array> {
193
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
194
+ throw new RangeError("stream heartbeat timeout must be a positive safe integer");
195
+ }
196
+ const reader = stream.getReader();
197
+ return new ReadableStream<Uint8Array>({
198
+ pull: async (controller) => {
199
+ let timer: ReturnType<typeof setTimeout> | undefined;
200
+ let abortListener: (() => void) | undefined;
201
+ try {
202
+ const result = await Promise.race([
203
+ reader.read(),
204
+ new Promise<never>((_resolve, reject) => {
205
+ timer = setTimeout(
206
+ () => reject(new TypeError(`event stream heartbeat timed out after ${timeoutMs}ms`)),
207
+ timeoutMs,
208
+ );
209
+ if (signal) {
210
+ abortListener = () => reject(new DOMException("Aborted", "AbortError"));
211
+ signal.addEventListener("abort", abortListener, { once: true });
212
+ }
213
+ }),
214
+ ]);
215
+ if (result.done) controller.close();
216
+ else controller.enqueue(result.value);
217
+ } catch (error) {
218
+ void reader.cancel(error).catch(() => {});
219
+ controller.error(error);
220
+ } finally {
221
+ if (timer !== undefined) clearTimeout(timer);
222
+ if (signal && abortListener) signal.removeEventListener("abort", abortListener);
223
+ }
224
+ },
225
+ cancel: async (reason) => {
226
+ await reader.cancel(reason);
227
+ },
228
+ });
229
+ }
230
+
133
231
  /**
134
232
  * Yield the durable events with `fromExclusive < sequence <= toInclusive`,
135
233
  * in order. Sequences are contiguous, so every one of them must exist in the
@@ -159,7 +159,7 @@ export type TranscriptionResultMetadata = {
159
159
  export type TranscriptionDiagnostic = {
160
160
  operation: "start" | "session" | "cancel" | "close";
161
161
  code: TranscriptionErrorCode;
162
- /** Diagnostic-only detail. React sanitizes and bounds this before forwarding it. */
162
+ /** Exact diagnostic detail forwarded by React without content rewriting. */
163
163
  detail: string;
164
164
  };
165
165
 
@@ -231,7 +231,7 @@ export type TranscriptionEventListener = (event: TranscriptionEvent) => void;
231
231
  export type TranscriptionAdapterStartContext = {
232
232
  /** Aborted on local cancellation, policy replacement, timeout, or unmount. */
233
233
  signal: AbortSignal;
234
- /** Non-UI observability seam; callers receive only bounded, redacted detail. */
234
+ /** Non-UI observability seam; callers receive the exact provider detail. */
235
235
  reportDiagnostic: (diagnostic: TranscriptionDiagnostic) => void;
236
236
  };
237
237
 
package/src/types.ts CHANGED
@@ -1587,6 +1587,8 @@ export type FsListResponse = {
1587
1587
  revision: number;
1588
1588
  truncated: boolean;
1589
1589
  };
1590
+ export type FsListBatchRequest = { requests: FsListRequest[] };
1591
+ export type FsListBatchResponse = { results: FsListResponse[] };
1590
1592
  export type FsReadRequest = {
1591
1593
  path: string;
1592
1594
  encoding?: FsEncoding;
@@ -1695,6 +1697,16 @@ export type GitDiffRequest = {
1695
1697
  maxBytesPerFile?: number;
1696
1698
  };
1697
1699
  export type GitDiffResponse = { files: GitFileDiff[]; revision: number };
1700
+ export type GitReadBatchItemRequest = {
1701
+ status: GitStatusRequest;
1702
+ diff?: GitDiffRequest;
1703
+ };
1704
+ export type GitReadBatchRequest = { requests: GitReadBatchItemRequest[] };
1705
+ export type GitReadBatchItemResponse = {
1706
+ status: GitStatusResponse;
1707
+ diff?: GitDiffResponse;
1708
+ };
1709
+ export type GitReadBatchResponse = { results: GitReadBatchItemResponse[] };
1698
1710
  export type GitLogRequest = {
1699
1711
  path?: string;
1700
1712
  ref?: string;
@@ -1755,6 +1767,9 @@ export type WorkspaceCaptureRepo = {
1755
1767
  behind: number;
1756
1768
  status: GitFileStatus[];
1757
1769
  diff: GitFileDiff[];
1770
+ /** Current branch vs the remote default branch. Absent on legacy captures or
1771
+ * repositories whose remote default ref could not be resolved. */
1772
+ branchDiff?: GitFileDiff[] | undefined;
1758
1773
  };
1759
1774
  export type WorkspaceCaptureDegradedReason =
1760
1775
  | "repository_discovery_command_failed"
@@ -1875,7 +1890,7 @@ export type SessionStructuredCapabilities = {
1875
1890
 
1876
1891
  export type ScheduledTaskStatus = "active" | "paused";
1877
1892
 
1878
- export type ScheduledTaskRunMode = "new_session_per_run" | "reusable_session";
1893
+ export type ScheduledTaskRunMode = "new_session_per_run" | "reusable_session" | "existing_session";
1879
1894
 
1880
1895
  export type ScheduledTaskOverlapPolicy = "allow_concurrent" | "skip" | "buffer_one";
1881
1896
 
@@ -1931,6 +1946,7 @@ export type ScheduledTask = {
1931
1946
  createdBy?: TurnInitiator | undefined;
1932
1947
  createdByContext?: TurnInitiatorContext | undefined;
1933
1948
  personalConnections?: McpPersonalConnectionSummary[] | undefined;
1949
+ targetSessionId: string | null;
1934
1950
  reusableSessionId: string | null;
1935
1951
  variableSetId: string | null;
1936
1952
  /** @deprecated use variableSetId */
@@ -2038,8 +2054,14 @@ export const KNOWN_PERMISSIONS = [
2038
2054
  "connections:write",
2039
2055
  "environments:manage",
2040
2056
  "environments:use",
2057
+ "variable-sets:list",
2058
+ "variable-sets:read",
2059
+ "variable-sets:write",
2041
2060
  "variable-sets:manage",
2042
2061
  "variable-sets:use",
2062
+ "secrets:list",
2063
+ "secrets:read",
2064
+ "secrets:write",
2043
2065
  "mcp_servers:attach",
2044
2066
  "toolspace:call",
2045
2067
  "goals:manage",
@@ -2075,6 +2097,8 @@ export type FirstPartyMcpToolName =
2075
2097
  | "sandbox_swap"
2076
2098
  | "run_on"
2077
2099
  | "sandbox_provision"
2100
+ | "connected_machine_remove"
2101
+ | "connected_machine_remove"
2078
2102
  | "rig_list"
2079
2103
  | "rig_get"
2080
2104
  | "rig_propose_change"
@@ -2091,6 +2115,7 @@ export type FirstPartyMcpToolName =
2091
2115
  | "set_other_session_title"
2092
2116
  | "variable_set_list"
2093
2117
  | "environment_list"
2118
+ | "variable_set_get_variable"
2094
2119
  | "variable_set_set_variable"
2095
2120
  | "environment_set_variable"
2096
2121
  | "github_connect_link"
@@ -2736,6 +2761,8 @@ export type WorkspaceSettings = {
2736
2761
  maxNestedAgentDepth?: number | null | undefined;
2737
2762
  /** Default for new Codex sessions; absent ⇒ remote_v2. */
2738
2763
  codexCompactionDefault?: "remote_v2" | "portable" | undefined;
2764
+ /** Whether agents may invoke the built-in structured human-input tool. */
2765
+ agentHumanInputEnabled?: boolean | undefined;
2739
2766
  slackReactionSummon?: WorkspaceSlackReactionSummonSettings | undefined;
2740
2767
  [key: string]: unknown;
2741
2768
  };
@@ -2767,6 +2794,7 @@ export type UpdateWorkspaceSettingsRequest = {
2767
2794
  transcription?: WorkspaceTranscriptionPolicy | undefined;
2768
2795
  maxNestedAgentDepth?: number | null | undefined;
2769
2796
  codexCompactionDefault?: "remote_v2" | "portable" | undefined;
2797
+ agentHumanInputEnabled?: boolean | undefined;
2770
2798
  slackReactionSummon?: WorkspaceSlackReactionSummonSettings | undefined;
2771
2799
  [key: string]: unknown;
2772
2800
  };
@@ -3179,6 +3207,7 @@ export type CreateScheduledTaskRequest = {
3179
3207
  name: string;
3180
3208
  schedule: ScheduledTaskScheduleSpec;
3181
3209
  runMode?: ScheduledTaskRunMode | undefined;
3210
+ targetSessionId?: string | null | undefined;
3182
3211
  overlapPolicy?: ScheduledTaskOverlapPolicy | undefined;
3183
3212
  agentConfig: ScheduledTaskAgentConfigInput;
3184
3213
  status?: ScheduledTaskStatus | undefined;
@@ -3194,6 +3223,7 @@ export type UpdateScheduledTaskRequest = {
3194
3223
  name?: string | undefined;
3195
3224
  schedule?: ScheduledTaskScheduleSpec | undefined;
3196
3225
  runMode?: ScheduledTaskRunMode | undefined;
3226
+ targetSessionId?: string | null | undefined;
3197
3227
  overlapPolicy?: ScheduledTaskOverlapPolicy | undefined;
3198
3228
  agentConfig?: ScheduledTaskAgentConfigInput | undefined;
3199
3229
  status?: ScheduledTaskStatus | undefined;
@@ -3227,11 +3257,7 @@ export type ScheduledTaskRun = {
3227
3257
 
3228
3258
  // --- VariableSets -------------------------------------------------------------
3229
3259
 
3230
- /**
3231
- * Variable values are write-only by design: the API never returns a value, so
3232
- * reads expose name + version metadata only. Values are decrypted exclusively
3233
- * inside the worker at sandbox materialization time.
3234
- */
3260
+ /** Generic variable-set reads expose name + version metadata only. */
3235
3261
  export type VariableSetVariableMetadata = {
3236
3262
  name: string;
3237
3263
  version: number;
@@ -3239,6 +3265,14 @@ export type VariableSetVariableMetadata = {
3239
3265
  updatedAt: string;
3240
3266
  };
3241
3267
 
3268
+ /** Dedicated permissioned plaintext response; never embedded in metadata reads. */
3269
+ export type VariableSetSecret = {
3270
+ variableSetId: string;
3271
+ name: string;
3272
+ version: number;
3273
+ value: string;
3274
+ };
3275
+
3242
3276
  export type VariableSet = {
3243
3277
  id: string;
3244
3278
  accountId: string;
@@ -3412,12 +3446,14 @@ export type FileAsset = {
3412
3446
  /** Mirrors the closed, provider-neutral retained-output contract. */
3413
3447
  export const RETAINED_OUTPUT_DEFAULT_PAGE_BYTES = 256 * 1024;
3414
3448
  export const RETAINED_OUTPUT_MAX_PAGE_BYTES = 1024 * 1024;
3449
+ export const COMPUTER_SCREENSHOT_MAX_BYTES = 32 * 1024 * 1024;
3415
3450
 
3416
3451
  export type RetainedOutputKind =
3417
3452
  | "tool_result"
3418
3453
  | "assistant_completion"
3419
3454
  | "internal_update"
3420
3455
  | "event_media"
3456
+ | "computer_screenshot"
3421
3457
  | "file";
3422
3458
 
3423
3459
  export type RetainedOutputUnavailableReason =
@@ -3428,6 +3464,9 @@ export type RetainedOutputUnavailableReason =
3428
3464
  | "deleted"
3429
3465
  | "missing_storage"
3430
3466
  | "storage_write_failed"
3467
+ | "quota_exceeded"
3468
+ | "invalid_content"
3469
+ | "oversized"
3431
3470
  | "unsupported";
3432
3471
 
3433
3472
  export type RetainedArtifactReference = {
@@ -3438,7 +3477,10 @@ export type RetainedArtifactReference = {
3438
3477
  originalBytes: number;
3439
3478
  sha256: string;
3440
3479
  retainedAt: string;
3441
- retention: { policy: "workspace_file"; expiresAt: null };
3480
+ dimensions?: { width: number; height: number } | undefined;
3481
+ retention:
3482
+ | { policy: "workspace_file"; expiresAt: null }
3483
+ | { policy: "session_screenshot"; expiresAt: string };
3442
3484
  retrieval: {
3443
3485
  method: "GET";
3444
3486
  path: string;
@@ -3470,6 +3512,18 @@ export type RetainedArtifactContent = {
3470
3512
  acceptRanges: "bytes";
3471
3513
  };
3472
3514
 
3515
+ export type RetainedScreenshotDownloadOptions = {
3516
+ signal?: AbortSignal | undefined;
3517
+ /** Retry transient range failures; bounded to 0..3, default 2. */
3518
+ maxRetries?: number | undefined;
3519
+ };
3520
+
3521
+ export type RetainedScreenshotDownload = {
3522
+ metadata: RetainedArtifactMetadata;
3523
+ /** Null when metadata truth says the screenshot is unavailable. */
3524
+ bytes: Uint8Array | null;
3525
+ };
3526
+
3473
3527
  export type CreateFileUploadRequest = {
3474
3528
  filename: string;
3475
3529
  contentType: string;
@@ -3505,6 +3559,8 @@ export type UploadFileInput = {
3505
3559
  contentType: string;
3506
3560
  data: FileUploadData;
3507
3561
  sha256?: string | undefined;
3562
+ /** Optional deadline for the signed object-storage PUT. */
3563
+ timeoutMs?: number | undefined;
3508
3564
  };
3509
3565
 
3510
3566
  // --- Documents -------------------------------------------------------------------
@@ -4478,6 +4534,32 @@ export type MachineMetricsSeriesResponse = {
4478
4534
  samples: MetricSample[];
4479
4535
  };
4480
4536
 
4537
+ /** POST /v1/workspaces/:ws/enrollments/:id/revoke body. */
4538
+ export type RemoveEnrollmentRequest = {
4539
+ expectedUpdatedAt?: string;
4540
+ idempotencyKey?: string;
4541
+ };
4542
+
4543
+ /** Typed removal/revocation outcome. Blocked outcomes preserve the exact
4544
+ * dependency and the action needed to make removal safe. */
4545
+ export type RemoveEnrollmentResponse = {
4546
+ revoked: boolean;
4547
+ outcome: "removed" | "already_removed" | "blocked";
4548
+ enrollmentId: string;
4549
+ machineName: string | null;
4550
+ lastSeenAt: string | null;
4551
+ revokedAt: string | null;
4552
+ code:
4553
+ | "active_route"
4554
+ | "active_commands"
4555
+ | "active_lease"
4556
+ | "recovery_pending"
4557
+ | "not_selfhosted"
4558
+ | null;
4559
+ message: string;
4560
+ action: string;
4561
+ };
4562
+
4481
4563
  /** POST /v1/workspaces/:ws/sessions/:sessionId/active-sandbox — swap a session's
4482
4564
  * active sandbox. `target` is a `MachineView.sandboxId`, or "session"/"default"
4483
4565
  * to swap back to the session's own group box. */
@@ -1,6 +1,11 @@
1
1
  import { isAbortError, isRetryableStreamError, OpenGeniStreamError } from "./errors";
2
2
  import { parseSseStream } from "./sse";
3
- import type { StreamSessionEventsOptions } from "./stream";
3
+ import {
4
+ jitteredDelay,
5
+ runBeforeLive,
6
+ type StreamSessionEventsOptions,
7
+ withStreamInactivityTimeout,
8
+ } from "./stream";
4
9
  import type { WorkspaceControlEvent } from "./types";
5
10
 
6
11
  export type WorkspaceControlStreamTransport = {
@@ -24,6 +29,9 @@ export async function* streamWorkspaceControlEvents(
24
29
  const reconnect = options.reconnect ?? true;
25
30
  const baseDelayMs = options.reconnectDelayMs ?? 500;
26
31
  const maxDelayMs = options.maxReconnectDelayMs ?? 10_000;
32
+ const jitterRatio = options.reconnectJitterRatio ?? 0.2;
33
+ const beforeLiveTimeoutMs = options.beforeLiveTimeoutMs ?? 15_000;
34
+ const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? 45_000;
27
35
  const maxAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
28
36
  let cursor = options.after ?? 0;
29
37
  let failures = 0;
@@ -39,9 +47,11 @@ export async function* streamWorkspaceControlEvents(
39
47
  everConnected = true;
40
48
  failures = 0;
41
49
  delayMs = baseDelayMs;
42
- await options.beforeLive?.();
50
+ await runBeforeLive(options.beforeLive, beforeLiveTimeoutMs, signal);
43
51
  options.onStateChange?.("live");
44
- for await (const message of parseSseStream(body)) {
52
+ for await (const message of parseSseStream(
53
+ withStreamInactivityTimeout(body, heartbeatTimeoutMs, signal),
54
+ )) {
45
55
  if (signal?.aborted) return;
46
56
  const event = parseWorkspaceControlEvent(message.data);
47
57
  if (!event || event.sequence <= cursor) continue;
@@ -49,7 +59,7 @@ export async function* streamWorkspaceControlEvents(
49
59
  yield event;
50
60
  }
51
61
  if (!reconnect) return;
52
- if (cursor === cursorAtOpen) await sleep(baseDelayMs, signal);
62
+ if (cursor === cursorAtOpen) await sleep(jitteredDelay(baseDelayMs, jitterRatio), signal);
53
63
  continue;
54
64
  } catch (error) {
55
65
  if (signal?.aborted || isAbortError(error)) return;
@@ -63,7 +73,7 @@ export async function* streamWorkspaceControlEvents(
63
73
  );
64
74
  }
65
75
  }
66
- await sleep(delayMs, signal);
76
+ await sleep(jitteredDelay(delayMs, jitterRatio), signal);
67
77
  delayMs = Math.min(Math.max(delayMs * 2, baseDelayMs), maxDelayMs);
68
78
  }
69
79
  }