@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/index.ts CHANGED
@@ -578,6 +578,8 @@ export type {
578
578
  FsEncoding,
579
579
  FsListRequest,
580
580
  FsListResponse,
581
+ FsListBatchRequest,
582
+ FsListBatchResponse,
581
583
  FsReadRequest,
582
584
  FsReadResponse,
583
585
  FsWriteRequest,
@@ -598,6 +600,10 @@ export type {
598
600
  GitFileDiff,
599
601
  GitDiffRequest,
600
602
  GitDiffResponse,
603
+ GitReadBatchItemRequest,
604
+ GitReadBatchRequest,
605
+ GitReadBatchItemResponse,
606
+ GitReadBatchResponse,
601
607
  GitLogRequest,
602
608
  GitCommit,
603
609
  GitLogResponse,
@@ -646,6 +652,7 @@ export type {
646
652
  Workspace,
647
653
  WorkspaceControlEvent,
648
654
  VariableSet,
655
+ VariableSetSecret,
649
656
  VariableSetVariableMetadata,
650
657
  Rig,
651
658
  RigVersion,
@@ -677,6 +684,8 @@ export type {
677
684
  MachineView,
678
685
  MachinesResponse,
679
686
  MachineMetricsSeriesResponse,
687
+ RemoveEnrollmentRequest,
688
+ RemoveEnrollmentResponse,
680
689
  // Bring-your-own-compute: the user-authenticated active-sandbox swap (M7).
681
690
  SwapActiveSandboxRequest,
682
691
  SwapActiveSandboxResponse,
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"
@@ -2568,6 +2593,98 @@ export type ClientVoiceInputConfig = {
2568
2593
  maxDurationSeconds: number;
2569
2594
  maxSizeBytes: number;
2570
2595
  acceptedMimeTypes: string[];
2596
+ resumable?: ClientResumableVoiceInputConfig | undefined;
2597
+ };
2598
+
2599
+ export type ClientResumableVoiceInputConfig = {
2600
+ maxDurationSeconds: number;
2601
+ maxSizeBytes: number;
2602
+ maxChunkSizeBytes: number;
2603
+ providerSegmentSeconds: number;
2604
+ };
2605
+
2606
+ export type TranscriptionRecordingErrorCode =
2607
+ | "permission_denied"
2608
+ | "not_supported"
2609
+ | "network"
2610
+ | "provider"
2611
+ | "policy_blocked"
2612
+ | "timeout"
2613
+ | "cancelled"
2614
+ | "unavailable"
2615
+ | "too_large"
2616
+ | "invalid_audio"
2617
+ | "unknown";
2618
+
2619
+ export type TranscriptionRecordingState =
2620
+ | "uploading"
2621
+ | "segmenting"
2622
+ | "ready"
2623
+ | "transcribing"
2624
+ | "complete"
2625
+ | "failed"
2626
+ | "discarded";
2627
+
2628
+ export type TranscriptionRecordingSegmentState =
2629
+ | "preparing"
2630
+ | "pending"
2631
+ | "transcribing"
2632
+ | "complete"
2633
+ | "failed";
2634
+
2635
+ export type TranscriptionRecordingSegment = {
2636
+ segmentNumber: number;
2637
+ state: TranscriptionRecordingSegmentState;
2638
+ startMilliseconds: number;
2639
+ durationMilliseconds: number;
2640
+ byteLength: number;
2641
+ errorCode: TranscriptionRecordingErrorCode | null;
2642
+ retryable: boolean;
2643
+ };
2644
+
2645
+ export type TranscriptionRecording = {
2646
+ id: string;
2647
+ workspaceId: string;
2648
+ mimeType: string;
2649
+ state: TranscriptionRecordingState;
2650
+ nextChunkNumber: number;
2651
+ chunkCount: number;
2652
+ totalBytes: number;
2653
+ totalDurationMilliseconds: number;
2654
+ segmentCount: number;
2655
+ completedSegmentCount: number;
2656
+ transcriptText: string | null;
2657
+ languages: string[];
2658
+ errorCode: TranscriptionRecordingErrorCode | null;
2659
+ retryable: boolean;
2660
+ objectsCleaned: boolean;
2661
+ createdAt: string;
2662
+ updatedAt: string;
2663
+ expiresAt: string;
2664
+ };
2665
+
2666
+ export type TranscriptionRecordingResponse = {
2667
+ recording: TranscriptionRecording;
2668
+ segments: TranscriptionRecordingSegment[];
2669
+ retryAfterMilliseconds?: number;
2670
+ };
2671
+
2672
+ export type TranscriptionRecordingListResponse = {
2673
+ recordings: TranscriptionRecording[];
2674
+ };
2675
+
2676
+ export type TranscriptionRecordingChunk = {
2677
+ chunkNumber: number;
2678
+ byteLength: number;
2679
+ sha256: string;
2680
+ startMilliseconds: number;
2681
+ durationMilliseconds: number;
2682
+ deduplicated: boolean;
2683
+ };
2684
+
2685
+ export type UploadTranscriptionRecordingChunkResponse = {
2686
+ recording: TranscriptionRecording;
2687
+ chunk: TranscriptionRecordingChunk;
2571
2688
  };
2572
2689
 
2573
2690
  /** Response from POST /v1/workspaces/:workspaceId/transcriptions. */
@@ -2644,6 +2761,8 @@ export type WorkspaceSettings = {
2644
2761
  maxNestedAgentDepth?: number | null | undefined;
2645
2762
  /** Default for new Codex sessions; absent ⇒ remote_v2. */
2646
2763
  codexCompactionDefault?: "remote_v2" | "portable" | undefined;
2764
+ /** Whether agents may invoke the built-in structured human-input tool. */
2765
+ agentHumanInputEnabled?: boolean | undefined;
2647
2766
  slackReactionSummon?: WorkspaceSlackReactionSummonSettings | undefined;
2648
2767
  [key: string]: unknown;
2649
2768
  };
@@ -2675,6 +2794,7 @@ export type UpdateWorkspaceSettingsRequest = {
2675
2794
  transcription?: WorkspaceTranscriptionPolicy | undefined;
2676
2795
  maxNestedAgentDepth?: number | null | undefined;
2677
2796
  codexCompactionDefault?: "remote_v2" | "portable" | undefined;
2797
+ agentHumanInputEnabled?: boolean | undefined;
2678
2798
  slackReactionSummon?: WorkspaceSlackReactionSummonSettings | undefined;
2679
2799
  [key: string]: unknown;
2680
2800
  };
@@ -3087,6 +3207,7 @@ export type CreateScheduledTaskRequest = {
3087
3207
  name: string;
3088
3208
  schedule: ScheduledTaskScheduleSpec;
3089
3209
  runMode?: ScheduledTaskRunMode | undefined;
3210
+ targetSessionId?: string | null | undefined;
3090
3211
  overlapPolicy?: ScheduledTaskOverlapPolicy | undefined;
3091
3212
  agentConfig: ScheduledTaskAgentConfigInput;
3092
3213
  status?: ScheduledTaskStatus | undefined;
@@ -3102,6 +3223,7 @@ export type UpdateScheduledTaskRequest = {
3102
3223
  name?: string | undefined;
3103
3224
  schedule?: ScheduledTaskScheduleSpec | undefined;
3104
3225
  runMode?: ScheduledTaskRunMode | undefined;
3226
+ targetSessionId?: string | null | undefined;
3105
3227
  overlapPolicy?: ScheduledTaskOverlapPolicy | undefined;
3106
3228
  agentConfig?: ScheduledTaskAgentConfigInput | undefined;
3107
3229
  status?: ScheduledTaskStatus | undefined;
@@ -3135,11 +3257,7 @@ export type ScheduledTaskRun = {
3135
3257
 
3136
3258
  // --- VariableSets -------------------------------------------------------------
3137
3259
 
3138
- /**
3139
- * Variable values are write-only by design: the API never returns a value, so
3140
- * reads expose name + version metadata only. Values are decrypted exclusively
3141
- * inside the worker at sandbox materialization time.
3142
- */
3260
+ /** Generic variable-set reads expose name + version metadata only. */
3143
3261
  export type VariableSetVariableMetadata = {
3144
3262
  name: string;
3145
3263
  version: number;
@@ -3147,6 +3265,14 @@ export type VariableSetVariableMetadata = {
3147
3265
  updatedAt: string;
3148
3266
  };
3149
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
+
3150
3276
  export type VariableSet = {
3151
3277
  id: string;
3152
3278
  accountId: string;
@@ -3320,12 +3446,14 @@ export type FileAsset = {
3320
3446
  /** Mirrors the closed, provider-neutral retained-output contract. */
3321
3447
  export const RETAINED_OUTPUT_DEFAULT_PAGE_BYTES = 256 * 1024;
3322
3448
  export const RETAINED_OUTPUT_MAX_PAGE_BYTES = 1024 * 1024;
3449
+ export const COMPUTER_SCREENSHOT_MAX_BYTES = 32 * 1024 * 1024;
3323
3450
 
3324
3451
  export type RetainedOutputKind =
3325
3452
  | "tool_result"
3326
3453
  | "assistant_completion"
3327
3454
  | "internal_update"
3328
3455
  | "event_media"
3456
+ | "computer_screenshot"
3329
3457
  | "file";
3330
3458
 
3331
3459
  export type RetainedOutputUnavailableReason =
@@ -3336,6 +3464,9 @@ export type RetainedOutputUnavailableReason =
3336
3464
  | "deleted"
3337
3465
  | "missing_storage"
3338
3466
  | "storage_write_failed"
3467
+ | "quota_exceeded"
3468
+ | "invalid_content"
3469
+ | "oversized"
3339
3470
  | "unsupported";
3340
3471
 
3341
3472
  export type RetainedArtifactReference = {
@@ -3346,7 +3477,10 @@ export type RetainedArtifactReference = {
3346
3477
  originalBytes: number;
3347
3478
  sha256: string;
3348
3479
  retainedAt: string;
3349
- 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 };
3350
3484
  retrieval: {
3351
3485
  method: "GET";
3352
3486
  path: string;
@@ -3378,6 +3512,18 @@ export type RetainedArtifactContent = {
3378
3512
  acceptRanges: "bytes";
3379
3513
  };
3380
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
+
3381
3527
  export type CreateFileUploadRequest = {
3382
3528
  filename: string;
3383
3529
  contentType: string;
@@ -3413,6 +3559,8 @@ export type UploadFileInput = {
3413
3559
  contentType: string;
3414
3560
  data: FileUploadData;
3415
3561
  sha256?: string | undefined;
3562
+ /** Optional deadline for the signed object-storage PUT. */
3563
+ timeoutMs?: number | undefined;
3416
3564
  };
3417
3565
 
3418
3566
  // --- Documents -------------------------------------------------------------------
@@ -4386,6 +4534,32 @@ export type MachineMetricsSeriesResponse = {
4386
4534
  samples: MetricSample[];
4387
4535
  };
4388
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
+
4389
4563
  /** POST /v1/workspaces/:ws/sessions/:sessionId/active-sandbox — swap a session's
4390
4564
  * active sandbox. `target` is a `MachineView.sandboxId`, or "session"/"default"
4391
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
  }