@opengeni/sdk 0.15.0 → 0.23.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
@@ -16,6 +16,8 @@ import type {
16
16
  CodexAccount,
17
17
  CodexAccountsResponse,
18
18
  CodexRotationSettings,
19
+ CodexOverviewResponse,
20
+ CodexAllocatorUpdate,
19
21
  CodexConnectionStatus,
20
22
  CodexConnectPoll,
21
23
  CodexConnectStart,
@@ -28,6 +30,7 @@ import type {
28
30
  CapabilityInstallation,
29
31
  AddDocumentRequest,
30
32
  ClientConfig,
33
+ WorkspaceModelCatalogResponse,
31
34
  ClientSessionEventInput,
32
35
  CompactSessionContextResult,
33
36
  CompleteFileUploadResponse,
@@ -46,6 +49,7 @@ import type {
46
49
  CreateKnowledgeMemoryRequest,
47
50
  CreateScheduledTaskRequest,
48
51
  CreateSessionRequest,
52
+ CreateSessionResponse,
49
53
  CreateVariableSetRequest,
50
54
  CreateRigRequest,
51
55
  CreateWorkspaceRequest,
@@ -81,6 +85,9 @@ import type {
81
85
  ListWorkspaceMembersResponse,
82
86
  PackInstallation,
83
87
  ReasoningEffort,
88
+ RetainedArtifactContent,
89
+ RetainedArtifactContentOptions,
90
+ RetainedArtifactMetadata,
84
91
  RegisterCapabilityPackRequest,
85
92
  ResourceRef,
86
93
  ScheduledTask,
@@ -89,9 +96,16 @@ import type {
89
96
  SessionListResponse,
90
97
  UpdateSessionPinRequest,
91
98
  SessionEvent,
99
+ SessionEventCompactResult,
100
+ SessionEventCompactResultOptions,
101
+ SessionEventListOptions,
102
+ SessionEventPage,
92
103
  SessionGoal,
104
+ SessionHumanInputRequest,
93
105
  SessionLineageResponse,
94
106
  SessionMcpCredentialUpdateInput,
107
+ UpdateSessionMcpApprovalPolicyRequest,
108
+ UpdateSessionMcpApprovalPolicyResponse,
95
109
  SessionQueueSnapshot,
96
110
  SessionQueueMutationResponse,
97
111
  ComposerDraft,
@@ -104,6 +118,7 @@ import type {
104
118
  WorkspaceInferenceControlResponse,
105
119
  WorkspaceControlEvent,
106
120
  SessionTurn,
121
+ SubmitHumanInputResponseRequest,
107
122
  // Stream surfacing (Phase 5): capability negotiation + viewer lifecycle + config.
108
123
  SessionCapabilities,
109
124
  AttachViewerRequest,
@@ -133,7 +148,7 @@ import type {
133
148
  GitLogResponse,
134
149
  GitShowRequest,
135
150
  GitShowResponse,
136
- // Workbench v2 turn-end capture reads (M2, dossier §10.3).
151
+ // Workbench v2 turn-end capture reads (M2).
137
152
  GetWorkspaceCaptureResponse,
138
153
  GetWorkspaceCaptureFileResponse,
139
154
  TerminalExecRequest,
@@ -172,10 +187,21 @@ import type {
172
187
  OAuthStartRequest,
173
188
  OAuthStartResponse,
174
189
  } from "./types";
175
- import { OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION } from "./types";
190
+ import {
191
+ OPENGENI_API_CONTRACT_HEADER,
192
+ OPENGENI_API_CONTRACT_REVISION,
193
+ RETAINED_OUTPUT_MAX_PAGE_BYTES,
194
+ } from "./types";
176
195
 
177
196
  export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
178
197
 
198
+ export type WorkspaceControlEventPage = {
199
+ events: WorkspaceControlEvent[];
200
+ bytes: number;
201
+ truncated: boolean;
202
+ nextAfter: number | null;
203
+ };
204
+
179
205
  export type OpenGeniClientOptions = {
180
206
  /** Base URL of the OpenGeni API, e.g. `https://api.example.com`. */
181
207
  baseUrl: string;
@@ -187,8 +213,15 @@ export type OpenGeniClientOptions = {
187
213
  fetch?: FetchLike;
188
214
  };
189
215
 
216
+ /** Per-request cancellation for identity-scoped, side-effect-free reads. */
217
+ export type OpenGeniRequestOptions = {
218
+ signal?: AbortSignal | undefined;
219
+ };
220
+
190
221
  export type SendMessageInput = {
191
222
  text: string;
223
+ /** System instructions scoped to this exact turn; never visible timeline text. */
224
+ turnInstructions?: string;
192
225
  resources?: ResourceRef[];
193
226
  tools?: ToolRef[];
194
227
  model?: string;
@@ -225,8 +258,11 @@ export class OpenGeniClient {
225
258
 
226
259
  // --- Session lifecycle ---------------------------------------------------
227
260
 
228
- async createSession(workspaceId: string, request: CreateSessionRequest): Promise<Session> {
229
- return await this.requestJson<Session>(
261
+ async createSession(
262
+ workspaceId: string,
263
+ request: CreateSessionRequest,
264
+ ): Promise<CreateSessionResponse> {
265
+ return await this.requestJson<CreateSessionResponse>(
230
266
  "POST",
231
267
  `/v1/workspaces/${workspaceId}/sessions`,
232
268
  request,
@@ -252,6 +288,24 @@ export class OpenGeniClient {
252
288
  );
253
289
  }
254
290
 
291
+ /**
292
+ * Replace one attached MCP server's approval policy. The change is captured
293
+ * by the next claimed attempt; already-claimed work keeps its immutable
294
+ * policy snapshot.
295
+ */
296
+ async updateSessionMcpApprovalPolicy(
297
+ workspaceId: string,
298
+ sessionId: string,
299
+ serverId: string,
300
+ request: UpdateSessionMcpApprovalPolicyRequest,
301
+ ): Promise<UpdateSessionMcpApprovalPolicyResponse> {
302
+ return await this.requestJson<UpdateSessionMcpApprovalPolicyResponse>(
303
+ "PATCH",
304
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/mcp-servers/${encodeURIComponent(serverId)}/approval-policy`,
305
+ request,
306
+ );
307
+ }
308
+
255
309
  async listSessions(
256
310
  workspaceId: string,
257
311
  options: {
@@ -353,7 +407,7 @@ export class OpenGeniClient {
353
407
  */
354
408
  async listMachines(
355
409
  workspaceId: string,
356
- options: { sessionId?: string } = {},
410
+ options: { sessionId?: string; signal?: AbortSignal } = {},
357
411
  ): Promise<MachinesResponse> {
358
412
  return await this.requestJson<MachinesResponse>(
359
413
  "GET",
@@ -362,6 +416,7 @@ export class OpenGeniClient {
362
416
  {
363
417
  ...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
364
418
  },
419
+ { signal: options.signal },
365
420
  );
366
421
  }
367
422
 
@@ -493,27 +548,142 @@ export class OpenGeniClient {
493
548
  // --- Events: replay, send, stream ----------------------------------------
494
549
 
495
550
  /**
496
- * Replay durable events by sequence, ascending. `before` is exclusive and
497
- * returns the newest matching window. With `compact`, consecutive delta runs
498
- * may be coalesced; `payload.coalescedUntil` carries the run's last sequence
499
- * for resume cursors.
551
+ * Return the events from one bounded page. With no cursor, this uses the safe
552
+ * semantic monitoring tail; pass explicit forensic options and a cursor for
553
+ * retained audit replay. Use `listEventPage` when projection, coverage, or
554
+ * resume-cursor facts are required.
500
555
  */
501
556
  async listEvents(
502
557
  workspaceId: string,
503
558
  sessionId: string,
504
- options: { after?: number; before?: number; limit?: number; compact?: boolean } = {},
559
+ options: SessionEventListOptions = {},
505
560
  ): Promise<SessionEvent[]> {
506
- return await this.requestJson<SessionEvent[]>(
507
- "GET",
508
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`,
509
- undefined,
561
+ return (await this.listEventPage(workspaceId, sessionId, options)).events;
562
+ }
563
+
564
+ /** Bounded durable/monitoring page plus exact projection and cursor facts. */
565
+ async listEventPage(
566
+ workspaceId: string,
567
+ sessionId: string,
568
+ options: SessionEventCompactResultOptions,
569
+ ): Promise<SessionEventCompactResult | null>;
570
+ async listEventPage(
571
+ workspaceId: string,
572
+ sessionId: string,
573
+ options?: SessionEventListOptions,
574
+ ): Promise<SessionEventPage>;
575
+ async listEventPage(
576
+ workspaceId: string,
577
+ sessionId: string,
578
+ options: SessionEventListOptions | SessionEventCompactResultOptions = {},
579
+ ): Promise<SessionEventPage | SessionEventCompactResult | null> {
580
+ if (
581
+ options.latest &&
582
+ ["includeTypes", "excludeTypes", "includeClasses", "excludeClasses"].some((name) =>
583
+ Object.prototype.hasOwnProperty.call(options, name),
584
+ )
585
+ ) {
586
+ throw new TypeError("latest cannot be combined with event filters");
587
+ }
588
+ if (options.resultMode === "compact" && !options.latest) {
589
+ throw new TypeError("resultMode=compact requires latest");
590
+ }
591
+ const listOptions: SessionEventListOptions | null =
592
+ options.resultMode === "compact" ? null : options;
593
+ const response = await this.fetchImpl(
594
+ this.url(`/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, {
595
+ ...(listOptions?.after !== undefined ? { after: String(listOptions.after) } : {}),
596
+ ...(listOptions?.before !== undefined ? { before: String(listOptions.before) } : {}),
597
+ ...(listOptions?.limit !== undefined ? { limit: String(listOptions.limit) } : {}),
598
+ ...(listOptions?.compact ? { compact: "1" } : {}),
599
+ ...(options.mode ? { mode: options.mode } : {}),
600
+ ...(listOptions?.direction ? { direction: listOptions.direction } : {}),
601
+ ...(options.payloadMode ? { payloadMode: options.payloadMode } : {}),
602
+ ...(options.resultMode ? { resultMode: options.resultMode } : {}),
603
+ ...(listOptions?.includeTypes?.length
604
+ ? { includeTypes: listOptions.includeTypes.join(",") }
605
+ : {}),
606
+ ...(listOptions?.excludeTypes?.length
607
+ ? { excludeTypes: listOptions.excludeTypes.join(",") }
608
+ : {}),
609
+ ...(listOptions?.includeClasses?.length
610
+ ? { includeClasses: listOptions.includeClasses.join(",") }
611
+ : {}),
612
+ ...(listOptions?.excludeClasses?.length
613
+ ? { excludeClasses: listOptions.excludeClasses.join(",") }
614
+ : {}),
615
+ ...(options.latest ? { latest: options.latest } : {}),
616
+ }),
510
617
  {
511
- ...(options.after !== undefined ? { after: String(options.after) } : {}),
512
- ...(options.before !== undefined ? { before: String(options.before) } : {}),
513
- ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
514
- ...(options.compact ? { compact: "1" } : {}),
618
+ method: "GET",
619
+ headers: { ...this.headers(), Accept: "application/json" },
515
620
  },
516
621
  );
622
+ assertApiContractResponse(response);
623
+ if (!response.ok) throw new OpenGeniApiError(response.status, await safeText(response));
624
+ const body = await response.json();
625
+ if (options.resultMode === "compact") {
626
+ return body as SessionEventCompactResult;
627
+ }
628
+ const events = body as SessionEvent[];
629
+ const integerHeader = (name: string): number | null => {
630
+ const raw = response.headers.get(name);
631
+ if (raw === null) return null;
632
+ const value = Number(raw);
633
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
634
+ };
635
+ const mode =
636
+ response.headers.get("X-OpenGeni-Event-Mode") === "forensic" ? "forensic" : "monitoring";
637
+ const direction =
638
+ response.headers.get("X-OpenGeni-Event-Direction") === "after" ? "after" : "before";
639
+ const payloadHeader = response.headers.get("X-OpenGeni-Payload-Mode");
640
+ const payloadMode =
641
+ payloadHeader === "none" || payloadHeader === "full" ? payloadHeader : "summary";
642
+ const first = integerHeader("X-OpenGeni-Covered-First");
643
+ const last = integerHeader("X-OpenGeni-Covered-Last");
644
+ const bytes =
645
+ integerHeader("X-OpenGeni-Page-Bytes") ??
646
+ new TextEncoder().encode(JSON.stringify(events)).byteLength;
647
+ const maxBytes = integerHeader("X-OpenGeni-Page-Max-Bytes") ?? 1024 * 1024;
648
+ const truncatedByHeader = response.headers.get("X-OpenGeni-Truncated-By");
649
+ const truncatedBy =
650
+ truncatedByHeader === "count" ||
651
+ truncatedByHeader === "bytes" ||
652
+ truncatedByHeader === "http_bytes"
653
+ ? truncatedByHeader
654
+ : null;
655
+ return {
656
+ events,
657
+ mode,
658
+ payloadMode,
659
+ direction,
660
+ bytes,
661
+ maxBytes,
662
+ truncated: response.headers.get("X-OpenGeni-Page-Truncated") === "true",
663
+ hasMore: response.headers.get("X-OpenGeni-Has-More") === "true",
664
+ truncatedBy,
665
+ coveredSequence: first === null || last === null ? null : { first, last },
666
+ nextAfter: integerHeader("X-OpenGeni-Next-After"),
667
+ nextBefore: integerHeader("X-OpenGeni-Next-Before"),
668
+ forensicExact: response.headers.get("X-OpenGeni-Forensic-Exact") === "true",
669
+ };
670
+ }
671
+
672
+ /**
673
+ * Fetch the authoritative newest-sequence semantic result directly. This is
674
+ * the callback-loss recovery path: it reads one compact durable result and
675
+ * never creates a model turn. `latest: "receipt"` aliases `tool_receipt`;
676
+ * turn generation remains scoped retry metadata.
677
+ */
678
+ async getLatestEventResult(
679
+ workspaceId: string,
680
+ sessionId: string,
681
+ options: Omit<SessionEventCompactResultOptions, "resultMode"> = { latest: "terminal" },
682
+ ): Promise<SessionEventCompactResult | null> {
683
+ return await this.listEventPage(workspaceId, sessionId, {
684
+ ...options,
685
+ resultMode: "compact",
686
+ });
517
687
  }
518
688
 
519
689
  /** POST a user/control event to the session. Returns the accepted event. */
@@ -574,6 +744,47 @@ export class OpenGeniClient {
574
744
  });
575
745
  }
576
746
 
747
+ async listHumanInputRequests(
748
+ workspaceId: string,
749
+ sessionId: string,
750
+ options: {
751
+ status?: SessionHumanInputRequest["status"];
752
+ } = {},
753
+ ): Promise<SessionHumanInputRequest[]> {
754
+ const result = await this.requestJson<{ requests: SessionHumanInputRequest[] }>(
755
+ "GET",
756
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/human-input-requests`,
757
+ undefined,
758
+ options.status ? { status: options.status } : undefined,
759
+ );
760
+ return result.requests;
761
+ }
762
+
763
+ async getHumanInputRequest(
764
+ workspaceId: string,
765
+ sessionId: string,
766
+ requestId: string,
767
+ ): Promise<SessionHumanInputRequest> {
768
+ return await this.requestJson<SessionHumanInputRequest>(
769
+ "GET",
770
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/human-input-requests/${requestId}`,
771
+ );
772
+ }
773
+
774
+ async submitHumanInputResponse(
775
+ workspaceId: string,
776
+ sessionId: string,
777
+ requestId: string,
778
+ response: SubmitHumanInputResponseRequest,
779
+ options: { clientEventId?: string } = {},
780
+ ): Promise<SessionEvent> {
781
+ return await this.sendEvent(workspaceId, sessionId, {
782
+ type: "user.humanInputResponse",
783
+ ...(options.clientEventId ? { clientEventId: options.clientEventId } : {}),
784
+ payload: { requestId, response },
785
+ });
786
+ }
787
+
577
788
  /**
578
789
  * Live-stream a session's events with automatic reconnect, resume from the
579
790
  * last seen sequence, gap backfill, and duplicate suppression. See
@@ -754,15 +965,45 @@ export class OpenGeniClient {
754
965
  workspaceId: string,
755
966
  options: { after?: number; limit?: number } = {},
756
967
  ): Promise<WorkspaceControlEvent[]> {
757
- return await this.requestJson<WorkspaceControlEvent[]>(
758
- "GET",
759
- `/v1/workspaces/${workspaceId}/control-events`,
760
- undefined,
761
- {
968
+ return (await this.listWorkspaceControlEventPage(workspaceId, options)).events;
969
+ }
970
+
971
+ /** Count/byte-bounded page plus an explicit continuation cursor. */
972
+ async listWorkspaceControlEventPage(
973
+ workspaceId: string,
974
+ options: { after?: number; limit?: number } = {},
975
+ ): Promise<WorkspaceControlEventPage> {
976
+ const response = await this.fetchImpl(
977
+ this.url(`/v1/workspaces/${workspaceId}/control-events`, {
762
978
  ...(options.after !== undefined ? { after: String(options.after) } : {}),
763
979
  ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
980
+ }),
981
+ {
982
+ method: "GET",
983
+ headers: { ...this.headers(), Accept: "application/json" },
764
984
  },
765
985
  );
986
+ assertApiContractResponse(response);
987
+ if (!response.ok) {
988
+ throw new OpenGeniApiError(response.status, await safeText(response));
989
+ }
990
+ const events = (await response.json()) as WorkspaceControlEvent[];
991
+ const bytesHeader = response.headers.get("X-OpenGeni-Page-Bytes");
992
+ const nextHeader = response.headers.get("X-OpenGeni-Next-After");
993
+ const parsedBytes = bytesHeader === null ? Number.NaN : Number(bytesHeader);
994
+ const parsedNext = nextHeader === null ? null : Number(nextHeader);
995
+ return {
996
+ events,
997
+ bytes:
998
+ Number.isSafeInteger(parsedBytes) && parsedBytes >= 0
999
+ ? parsedBytes
1000
+ : new TextEncoder().encode(JSON.stringify(events)).byteLength,
1001
+ truncated: response.headers.get("X-OpenGeni-Page-Truncated") === "true",
1002
+ nextAfter:
1003
+ parsedNext !== null && Number.isSafeInteger(parsedNext) && parsedNext >= 0
1004
+ ? parsedNext
1005
+ : null,
1006
+ };
766
1007
  }
767
1008
 
768
1009
  streamWorkspaceControlEvents(
@@ -903,11 +1144,14 @@ export class OpenGeniClient {
903
1144
  workspaceId: string,
904
1145
  sessionId: string,
905
1146
  request: FsListRequest = {},
1147
+ options: OpenGeniRequestOptions = {},
906
1148
  ): Promise<FsListResponse> {
907
1149
  return await this.requestJson<FsListResponse>(
908
1150
  "POST",
909
1151
  `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/list`,
910
1152
  request,
1153
+ {},
1154
+ options,
911
1155
  );
912
1156
  }
913
1157
 
@@ -916,11 +1160,14 @@ export class OpenGeniClient {
916
1160
  workspaceId: string,
917
1161
  sessionId: string,
918
1162
  request: FsReadRequest,
1163
+ options: OpenGeniRequestOptions = {},
919
1164
  ): Promise<FsReadResponse> {
920
1165
  return await this.requestJson<FsReadResponse>(
921
1166
  "POST",
922
1167
  `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/read`,
923
1168
  request,
1169
+ {},
1170
+ options,
924
1171
  );
925
1172
  }
926
1173
 
@@ -981,11 +1228,14 @@ export class OpenGeniClient {
981
1228
  workspaceId: string,
982
1229
  sessionId: string,
983
1230
  request: GitStatusRequest = {},
1231
+ options: OpenGeniRequestOptions = {},
984
1232
  ): Promise<GitStatusResponse> {
985
1233
  return await this.requestJson<GitStatusResponse>(
986
1234
  "POST",
987
1235
  `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/status`,
988
1236
  request,
1237
+ {},
1238
+ options,
989
1239
  );
990
1240
  }
991
1241
 
@@ -994,11 +1244,14 @@ export class OpenGeniClient {
994
1244
  workspaceId: string,
995
1245
  sessionId: string,
996
1246
  request: GitDiffRequest = {},
1247
+ options: OpenGeniRequestOptions = {},
997
1248
  ): Promise<GitDiffResponse> {
998
1249
  return await this.requestJson<GitDiffResponse>(
999
1250
  "POST",
1000
1251
  `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/diff`,
1001
1252
  request,
1253
+ {},
1254
+ options,
1002
1255
  );
1003
1256
  }
1004
1257
 
@@ -1035,10 +1288,14 @@ export class OpenGeniClient {
1035
1288
  async getWorkspaceCapture(
1036
1289
  workspaceId: string,
1037
1290
  sessionId: string,
1291
+ options: OpenGeniRequestOptions = {},
1038
1292
  ): Promise<GetWorkspaceCaptureResponse> {
1039
1293
  return await this.requestJson<GetWorkspaceCaptureResponse>(
1040
1294
  "GET",
1041
1295
  `/v1/workspaces/${workspaceId}/sessions/${sessionId}/workspace/capture`,
1296
+ undefined,
1297
+ {},
1298
+ options,
1042
1299
  );
1043
1300
  }
1044
1301
 
@@ -1050,6 +1307,7 @@ export class OpenGeniClient {
1050
1307
  sessionId: string,
1051
1308
  path: string,
1052
1309
  revision?: number,
1310
+ options: OpenGeniRequestOptions = {},
1053
1311
  ): Promise<GetWorkspaceCaptureFileResponse> {
1054
1312
  const query: Record<string, string> = { path };
1055
1313
  if (revision !== undefined) query.revision = String(revision);
@@ -1058,6 +1316,7 @@ export class OpenGeniClient {
1058
1316
  `/v1/workspaces/${workspaceId}/sessions/${sessionId}/workspace/capture/file`,
1059
1317
  undefined,
1060
1318
  query,
1319
+ options,
1061
1320
  );
1062
1321
  }
1063
1322
 
@@ -1142,10 +1401,14 @@ export class OpenGeniClient {
1142
1401
  async getStreamCapabilities(
1143
1402
  workspaceId: string,
1144
1403
  sessionId: string,
1404
+ options: OpenGeniRequestOptions = {},
1145
1405
  ): Promise<SessionCapabilities> {
1146
1406
  return await this.requestJson<SessionCapabilities>(
1147
1407
  "GET",
1148
1408
  `/v1/workspaces/${workspaceId}/sessions/${sessionId}/stream-capabilities`,
1409
+ undefined,
1410
+ {},
1411
+ options,
1149
1412
  );
1150
1413
  }
1151
1414
 
@@ -1228,6 +1491,14 @@ export class OpenGeniClient {
1228
1491
  return config;
1229
1492
  }
1230
1493
 
1494
+ /** Authenticated model definitions plus workspace-specific selectability. */
1495
+ async getWorkspaceModelCatalog(workspaceId: string): Promise<WorkspaceModelCatalogResponse> {
1496
+ return await this.requestJson<WorkspaceModelCatalogResponse>(
1497
+ "GET",
1498
+ `/v1/workspaces/${workspaceId}/model-catalog`,
1499
+ );
1500
+ }
1501
+
1231
1502
  /** The caller's access context: subject, account + workspace grants, defaults. */
1232
1503
  async getAccessContext(): Promise<AccessContext> {
1233
1504
  return await this.requestJson<AccessContext>("GET", "/v1/access/me");
@@ -1688,6 +1959,80 @@ export class OpenGeniClient {
1688
1959
  );
1689
1960
  }
1690
1961
 
1962
+ /** Read provider-neutral retained evidence metadata; never returns a storage location. */
1963
+ async getRetainedArtifact(
1964
+ workspaceId: string,
1965
+ artifactId: string,
1966
+ ): Promise<RetainedArtifactMetadata> {
1967
+ return await this.requestJson<RetainedArtifactMetadata>(
1968
+ "GET",
1969
+ `/v1/workspaces/${workspaceId}/artifacts/${artifactId}`,
1970
+ );
1971
+ }
1972
+
1973
+ /**
1974
+ * Read at most one authenticated retained-evidence range from the API. This
1975
+ * deliberately does not use the ordinary signed file-download URL.
1976
+ */
1977
+ async getRetainedArtifactContent(
1978
+ workspaceId: string,
1979
+ artifactId: string,
1980
+ options: RetainedArtifactContentOptions = {},
1981
+ ): Promise<RetainedArtifactContent> {
1982
+ if (options.range && (options.range.length > 128 || /[^\x20-\x7e]/.test(options.range))) {
1983
+ throw new RangeError("retained artifact range must be at most 128 printable ASCII bytes");
1984
+ }
1985
+ const response = await this.fetchImpl(
1986
+ this.url(`/v1/workspaces/${workspaceId}/artifacts/${artifactId}/content`),
1987
+ {
1988
+ method: "GET",
1989
+ headers: {
1990
+ ...this.headers(),
1991
+ Accept: "application/octet-stream",
1992
+ ...(options.range ? { Range: options.range } : {}),
1993
+ },
1994
+ ...(options.signal ? { signal: options.signal } : {}),
1995
+ },
1996
+ );
1997
+ try {
1998
+ assertApiContractResponse(response);
1999
+ } catch (error) {
2000
+ await cancelResponseBody(response, "retained artifact API contract mismatch");
2001
+ throw error;
2002
+ }
2003
+ if (!response.ok) {
2004
+ throw new OpenGeniApiError(response.status, await safeBoundedText(response));
2005
+ }
2006
+ if (response.status !== 200 && response.status !== 206) {
2007
+ await cancelResponseBody(response, "unexpected retained artifact response status");
2008
+ throw new OpenGeniApiError(response.status, "unexpected retained artifact response status");
2009
+ }
2010
+ if (response.headers.get("accept-ranges") !== "bytes") {
2011
+ await cancelResponseBody(response, "retained artifact response omitted byte-range support");
2012
+ throw new OpenGeniApiError(502, "retained artifact response omitted byte-range support");
2013
+ }
2014
+ let declaredLength: number | null;
2015
+ try {
2016
+ declaredLength = parseBoundedContentLength(response.headers.get("content-length"));
2017
+ } catch (error) {
2018
+ await cancelResponseBody(response, "invalid retained artifact content-length");
2019
+ throw error;
2020
+ }
2021
+ const bytes = await readBoundedResponseBytes(
2022
+ response,
2023
+ RETAINED_OUTPUT_MAX_PAGE_BYTES,
2024
+ declaredLength,
2025
+ );
2026
+ return {
2027
+ bytes,
2028
+ status: response.status,
2029
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
2030
+ contentLength: bytes.byteLength,
2031
+ contentRange: response.headers.get("content-range"),
2032
+ acceptRanges: "bytes",
2033
+ };
2034
+ }
2035
+
1691
2036
  /** Mint a short-lived signed download URL for a ready file. */
1692
2037
  async createFileDownloadUrl(
1693
2038
  workspaceId: string,
@@ -2048,15 +2393,14 @@ export class OpenGeniClient {
2048
2393
 
2049
2394
  // --- GitHub ----------------------------------------------------------------------------------
2050
2395
 
2051
- /** GitHub App configuration status + a signed install URL when configured. */
2396
+ /** GitHub App configuration status; install/link URLs are null while new binding is disabled. */
2052
2397
  async getGitHubApp(workspaceId: string): Promise<GitHubAppInfo> {
2053
2398
  return await this.requestJson<GitHubAppInfo>("GET", `/v1/workspaces/${workspaceId}/github/app`);
2054
2399
  }
2055
2400
 
2056
2401
  /**
2057
- * Browser entry point that plants the CSRF cookie and forwards to GitHub's
2058
- * install page. Open this in a browser (it redirects); `state` comes from
2059
- * `getGitHubApp().installUrl` or a github_connect_link tool.
2402
+ * Compatibility URL for previously issued state. New installation binding is
2403
+ * disabled, so the endpoint validates state and terminates with HTTP 410.
2060
2404
  */
2061
2405
  githubConnectUrl(workspaceId: string, state: string): string {
2062
2406
  return this.url(`/v1/workspaces/${workspaceId}/github/connect`, { state });
@@ -2077,6 +2421,14 @@ export class OpenGeniClient {
2077
2421
  );
2078
2422
  }
2079
2423
 
2424
+ /** Remove one workspace binding without uninstalling the GitHub App itself. */
2425
+ async unlinkGitHubInstallation(workspaceId: string, installationId: number): Promise<void> {
2426
+ await this.requestVoid(
2427
+ "DELETE",
2428
+ `/v1/workspaces/${workspaceId}/github/installations/${installationId}`,
2429
+ );
2430
+ }
2431
+
2080
2432
  /** Build a GitHub App manifest + the GitHub URL to submit it to. */
2081
2433
  async createGitHubAppManifest(
2082
2434
  workspaceId: string,
@@ -2219,6 +2571,14 @@ export class OpenGeniClient {
2219
2571
  );
2220
2572
  }
2221
2573
 
2574
+ /** Live independently-settled quota + reset-credit overview for every account. */
2575
+ async codexOverview(workspaceId: string): Promise<CodexOverviewResponse> {
2576
+ return await this.requestJson<CodexOverviewResponse>(
2577
+ "GET",
2578
+ `/v1/workspaces/${workspaceId}/codex/overview`,
2579
+ );
2580
+ }
2581
+
2222
2582
  /** Disconnect ALL accounts (legacy workspace-wide). Prefer `disconnectCodexAccount`. */
2223
2583
  async codexDisconnect(workspaceId: string): Promise<{ disconnected: boolean }> {
2224
2584
  return await this.requestJson<{ disconnected: boolean }>(
@@ -2261,6 +2621,19 @@ export class OpenGeniClient {
2261
2621
  );
2262
2622
  }
2263
2623
 
2624
+ /** Toggle only NEW automatic allocations under independent allocator OCC. */
2625
+ async setCodexAccountAllocator(
2626
+ workspaceId: string,
2627
+ accountId: string,
2628
+ input: { enabled: boolean; expectedVersion: number },
2629
+ ): Promise<CodexAllocatorUpdate> {
2630
+ return await this.requestJson<CodexAllocatorUpdate>(
2631
+ "PATCH",
2632
+ `/v1/workspaces/${workspaceId}/codex/accounts/${accountId}/allocator`,
2633
+ input,
2634
+ );
2635
+ }
2636
+
2264
2637
  /** Disconnect ONE Codex account by id (re-picks active when the removed one was active). */
2265
2638
  async disconnectCodexAccount(
2266
2639
  workspaceId: string,
@@ -2303,6 +2676,7 @@ export class OpenGeniClient {
2303
2676
  path: string,
2304
2677
  body?: unknown,
2305
2678
  query: Record<string, string> = {},
2679
+ options: OpenGeniRequestOptions = {},
2306
2680
  ): Promise<T> {
2307
2681
  const response = await this.fetchImpl(this.url(path, query), {
2308
2682
  method,
@@ -2312,6 +2686,7 @@ export class OpenGeniClient {
2312
2686
  ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
2313
2687
  },
2314
2688
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
2689
+ ...(options.signal ? { signal: options.signal } : {}),
2315
2690
  });
2316
2691
  assertApiContractResponse(response);
2317
2692
  if (!response.ok) {
@@ -2352,3 +2727,69 @@ async function safeText(response: Response): Promise<string> {
2352
2727
  return "";
2353
2728
  }
2354
2729
  }
2730
+
2731
+ async function safeBoundedText(response: Response): Promise<string> {
2732
+ try {
2733
+ return new TextDecoder().decode(await readBoundedResponseBytes(response, 64 * 1024, null));
2734
+ } catch {
2735
+ return "";
2736
+ }
2737
+ }
2738
+
2739
+ async function cancelResponseBody(response: Response, reason: string): Promise<void> {
2740
+ await response.body?.cancel(reason).catch(() => undefined);
2741
+ }
2742
+
2743
+ function parseBoundedContentLength(value: string | null): number | null {
2744
+ if (value === null) return null;
2745
+ if (!/^\d+$/.test(value)) {
2746
+ throw new OpenGeniApiError(502, "invalid retained artifact content-length");
2747
+ }
2748
+ const length = Number(value);
2749
+ if (!Number.isSafeInteger(length) || length > RETAINED_OUTPUT_MAX_PAGE_BYTES) {
2750
+ throw new OpenGeniApiError(502, "retained artifact response exceeds the SDK byte limit");
2751
+ }
2752
+ return length;
2753
+ }
2754
+
2755
+ async function readBoundedResponseBytes(
2756
+ response: Response,
2757
+ maxBytes: number,
2758
+ expectedBytes: number | null,
2759
+ ): Promise<Uint8Array> {
2760
+ if (!response.body) {
2761
+ if (expectedBytes !== null && expectedBytes !== 0) {
2762
+ throw new OpenGeniApiError(502, "retained artifact response length mismatch");
2763
+ }
2764
+ return new Uint8Array();
2765
+ }
2766
+ const reader = response.body.getReader();
2767
+ const chunks: Uint8Array[] = [];
2768
+ let totalBytes = 0;
2769
+ try {
2770
+ while (true) {
2771
+ const { done, value } = await reader.read();
2772
+ if (done) break;
2773
+ totalBytes += value.byteLength;
2774
+ if (totalBytes > maxBytes) {
2775
+ await reader
2776
+ .cancel("retained artifact response exceeded the SDK byte limit")
2777
+ .catch(() => undefined);
2778
+ throw new OpenGeniApiError(502, "retained artifact response exceeds the SDK byte limit");
2779
+ }
2780
+ chunks.push(value);
2781
+ }
2782
+ } finally {
2783
+ reader.releaseLock();
2784
+ }
2785
+ if (expectedBytes !== null && totalBytes !== expectedBytes) {
2786
+ throw new OpenGeniApiError(502, "retained artifact response length mismatch");
2787
+ }
2788
+ const bytes = new Uint8Array(totalBytes);
2789
+ let offset = 0;
2790
+ for (const chunk of chunks) {
2791
+ bytes.set(chunk, offset);
2792
+ offset += chunk.byteLength;
2793
+ }
2794
+ return bytes;
2795
+ }