@opengeni/sdk 1.4.0 → 1.4.4

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.
Files changed (46) hide show
  1. package/README.md +12 -0
  2. package/dist/adapters/next.js +2 -2
  3. package/dist/artifacts.js +5 -5
  4. package/dist/browser.js +3 -3
  5. package/dist/chat/index.js +15 -9
  6. package/dist/chat/index.js.map +1 -1
  7. package/dist/{chunk-BVIIRSJY.js → chunk-6SWFOZUP.js} +3 -3
  8. package/dist/{chunk-LEBDXYMA.js → chunk-7A5ZDWQH.js} +2 -2
  9. package/dist/{chunk-GYM54GKM.js → chunk-DTIMLEPB.js} +1 -1
  10. package/dist/chunk-DTIMLEPB.js.map +1 -0
  11. package/dist/{chunk-CA2T7QT4.js → chunk-HORQJBLP.js} +2 -2
  12. package/dist/{chunk-NC57YMUO.js → chunk-IJWKWFSF.js} +1 -1
  13. package/dist/chunk-IJWKWFSF.js.map +1 -0
  14. package/dist/{chunk-ZNC44QEX.js → chunk-JOI7XCG6.js} +45 -14
  15. package/dist/chunk-JOI7XCG6.js.map +1 -0
  16. package/dist/{chunk-D534PIXF.js → chunk-MAFL52JD.js} +2 -2
  17. package/dist/{chunk-FYQAMXDW.js → chunk-OLLQBZAE.js} +111 -5
  18. package/dist/chunk-OLLQBZAE.js.map +1 -0
  19. package/dist/{chunk-MXV5O25O.js → chunk-W72BOLRJ.js} +2 -2
  20. package/dist/codex-realtime-controller.js +1 -1
  21. package/dist/core.js +4 -4
  22. package/dist/document-authority.js +4 -4
  23. package/dist/editable-artifacts.js +1 -1
  24. package/dist/index.js +9 -9
  25. package/dist/interaction.d.ts +5 -0
  26. package/dist/interaction.js +1 -1
  27. package/dist/realtime.js +1 -1
  28. package/dist/session-proxy.d.ts +29 -6
  29. package/dist/session-proxy.js +2 -2
  30. package/dist/site.js +4 -4
  31. package/dist/types.d.ts +17 -0
  32. package/package.json +3 -3
  33. package/src/chat/opengeni.ts +14 -3
  34. package/src/codex-realtime-controller.ts +65 -14
  35. package/src/interaction.ts +5 -0
  36. package/src/session-proxy.ts +189 -9
  37. package/src/types.ts +15 -0
  38. package/dist/chunk-FYQAMXDW.js.map +0 -1
  39. package/dist/chunk-GYM54GKM.js.map +0 -1
  40. package/dist/chunk-NC57YMUO.js.map +0 -1
  41. package/dist/chunk-ZNC44QEX.js.map +0 -1
  42. /package/dist/{chunk-BVIIRSJY.js.map → chunk-6SWFOZUP.js.map} +0 -0
  43. /package/dist/{chunk-LEBDXYMA.js.map → chunk-7A5ZDWQH.js.map} +0 -0
  44. /package/dist/{chunk-CA2T7QT4.js.map → chunk-HORQJBLP.js.map} +0 -0
  45. /package/dist/{chunk-D534PIXF.js.map → chunk-MAFL52JD.js.map} +0 -0
  46. /package/dist/{chunk-MXV5O25O.js.map → chunk-W72BOLRJ.js.map} +0 -0
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  EMPTY_FINAL_REPLY_NOTICE,
3
3
  OpenGeniEmbeddingClient,
4
4
  turnCompletedWithEmptyFinalReply
5
- } from "./chunk-D534PIXF.js";
5
+ } from "./chunk-MAFL52JD.js";
6
6
  import {
7
7
  DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY,
8
8
  authorizeTranscriptionAdapter,
@@ -35,9 +35,9 @@ import {
35
35
  parseCodexRealtimeV3Event,
36
36
  projectSessionRealtimeLifecycle,
37
37
  startCodexRealtimeWebrtc
38
- } from "./chunk-ZNC44QEX.js";
39
- import "./chunk-MXV5O25O.js";
40
- import "./chunk-CA2T7QT4.js";
38
+ } from "./chunk-JOI7XCG6.js";
39
+ import "./chunk-W72BOLRJ.js";
40
+ import "./chunk-HORQJBLP.js";
41
41
  import {
42
42
  humanizeModelSlug,
43
43
  isRawModelLabel,
@@ -67,7 +67,7 @@ import {
67
67
  resumeSequenceFromRequest,
68
68
  sessionEventsToSseResponse,
69
69
  sessionEventsToSseStream
70
- } from "./chunk-FYQAMXDW.js";
70
+ } from "./chunk-OLLQBZAE.js";
71
71
  import "./chunk-UM4ZXXJR.js";
72
72
  import {
73
73
  ToolRequestError,
@@ -92,7 +92,7 @@ import {
92
92
  isOpenGeniSiteBridgeRequestMessage,
93
93
  isSiteCatalogStaleError,
94
94
  sanitizeOpenGeniSiteToolCallRequest
95
- } from "./chunk-LEBDXYMA.js";
95
+ } from "./chunk-7A5ZDWQH.js";
96
96
  import {
97
97
  OpenGeniToolCallError,
98
98
  OpenGeniToolReapprovalRequiredError,
@@ -112,7 +112,7 @@ import {
112
112
  streamWorkspaceControlEvents,
113
113
  streamWorkspaceInteractionRevisions,
114
114
  streamWorkspaceLiveEvents
115
- } from "./chunk-BVIIRSJY.js";
115
+ } from "./chunk-6SWFOZUP.js";
116
116
  import "./chunk-VPVMLM7Q.js";
117
117
  import {
118
118
  parseSseStream,
@@ -133,7 +133,7 @@ import {
133
133
  RETAINED_OUTPUT_MAX_PAGE_BYTES,
134
134
  SANDBOX_FILE_ARTIFACT_MAX_BYTES,
135
135
  SESSION_EVENT_TYPES
136
- } from "./chunk-GYM54GKM.js";
136
+ } from "./chunk-DTIMLEPB.js";
137
137
  import {
138
138
  AttachedBrowserDeviceCollection,
139
139
  AttachedBrowserDeviceResource,
@@ -175,7 +175,7 @@ import {
175
175
  interactionControlFailureFromError,
176
176
  parseBrowserFrameMetadata,
177
177
  parseComputerFrameMetadata
178
- } from "./chunk-NC57YMUO.js";
178
+ } from "./chunk-IJWKWFSF.js";
179
179
  import {
180
180
  OpenGeniAllowanceExhaustedError,
181
181
  OpenGeniApiContractMismatchError,
@@ -973,6 +973,8 @@ export type ComputerSessionCapabilities = {
973
973
  screenCapture: boolean;
974
974
  semanticActions: boolean;
975
975
  pointerInput: boolean;
976
+ /** Causal count-2 input requires its exact confirmed first operation. Absent means unsupported. */
977
+ pointerClickContinuation?: boolean | undefined;
976
978
  keyboardInput: boolean;
977
979
  clipboard: boolean;
978
980
  backgroundActions: boolean;
@@ -1066,6 +1068,9 @@ export type ComputerAction = {
1066
1068
  deltaX?: number | undefined;
1067
1069
  deltaY?: number | undefined;
1068
1070
  button?: "left" | "right" | "middle" | undefined;
1071
+ /** Click only; 2 delivers one second click pair after a completed first click. */
1072
+ clickCount?: 1 | 2 | undefined;
1073
+ continuationOfOperationId?: string | undefined;
1069
1074
  } | {
1070
1075
  type: "keyboard";
1071
1076
  action: "type" | "press";
@@ -39,7 +39,7 @@ import {
39
39
  interactionControlFailureFromError,
40
40
  parseBrowserFrameMetadata,
41
41
  parseComputerFrameMetadata
42
- } from "./chunk-NC57YMUO.js";
42
+ } from "./chunk-IJWKWFSF.js";
43
43
  import "./chunk-V6GIY62B.js";
44
44
  export {
45
45
  AttachedBrowserDeviceCollection,
package/dist/realtime.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  sessionRealtimeOwnerStorageKey,
24
24
  sessionRealtimeOwnerStorageNamespace,
25
25
  startCodexRealtimeWebrtc
26
- } from "./chunk-ZNC44QEX.js";
26
+ } from "./chunk-JOI7XCG6.js";
27
27
  import {
28
28
  createGatewayRealtimeTransportStarter,
29
29
  createXaiSubscriptionRealtimeTransportStarter
@@ -82,14 +82,21 @@ export type SessionProxyCreateInput = {
82
82
  export type SessionProxyMessageInput = {
83
83
  /** Absent for `create`. */
84
84
  sessionId?: string | undefined;
85
- delivery: "create" | "send" | "steer" | "submit";
85
+ /**
86
+ * `realtime` is live voice: once when a call starts (refuse it, or rotate
87
+ * MCP credentials), and before each batch of finalized transcripts and spoken
88
+ * requests is saved (refuse it, or add `modelContext`).
89
+ */
90
+ delivery: "create" | "send" | "steer" | "submit" | "realtime";
86
91
  };
87
92
  /** Server-side additions the host attaches to one forwarded user message or response. */
88
93
  export type SessionProxyMessageExtras = {
89
94
  /**
90
95
  * Model-visible context for this message (current page, time zone, today's
91
96
  * date). Placed before any context the browser sent. Not secret. Ignored for
92
- * approval decisions and human-input responses, which are not new messages.
97
+ * approval decisions and human-input responses, which are not new messages,
98
+ * and when a live voice call starts. Keep it stable for the same message:
99
+ * a retried voice entry must carry the same context to be accepted.
93
100
  */
94
101
  modelContext?: string | undefined;
95
102
  /**
@@ -157,9 +164,10 @@ export type SessionProxyHandlerOptions = {
157
164
  createSession?: ((input: SessionProxyCreateInput, context: SessionProxyContext) => CreateSessionRequest | Response | Promise<CreateSessionRequest | Response>) | undefined;
158
165
  /**
159
166
  * Called before every forwarded user message (send, steer, composer submit,
160
- * and browser-started create), approval decision, and human-input response.
161
- * Return server-owned `modelContext` (messages only) and MCP credential
162
- * rotations, or a `Response` to reject the action.
167
+ * and browser-started create), approval decision, human-input response, and
168
+ * live voice start and transcript save (`delivery: "realtime"`).
169
+ * Return server-owned `modelContext` (messages and voice transcripts only)
170
+ * and MCP credential rotations, or a `Response` to reject the action.
163
171
  */
164
172
  beforeForwardMessage?: ((input: SessionProxyMessageInput, context: SessionProxyContext) => SessionProxyMessageExtras | Response | undefined | Promise<SessionProxyMessageExtras | Response | undefined>) | undefined;
165
173
  /**
@@ -192,6 +200,18 @@ export type SessionProxyHandlerOptions = {
192
200
  * hide the microphone. Defaults to true.
193
201
  */
194
202
  voiceInput?: boolean | undefined;
203
+ /**
204
+ * Forward live speech-to-speech voice for existing chats as the resolved
205
+ * user: the voice model catalog and the call lifecycle routes under
206
+ * `.../sessions/{id}/realtime`. Opengeni still requires that user's
207
+ * `sessions:control` permission, binds the call to that user and browser,
208
+ * runs spoken requests as ordinary steers of the chat, and meters
209
+ * deployment-funded voice against the organization's credits. The stock
210
+ * composer shows the voice button only when a voice model is available.
211
+ * `false` reports `realtimeVoice: false` in the client config so stock UIs
212
+ * hide it, and refuses the routes. Defaults to true.
213
+ */
214
+ realtimeVoice?: boolean | undefined;
195
215
  /**
196
216
  * Let the browser read a file from the session's sandbox (`POST .../fs/read`)
197
217
  * so `sandbox:` links in agent replies can be downloaded. Only `path`,
@@ -248,7 +268,10 @@ export type SessionProxyHandlerOptions = {
248
268
  * settings decide which models end users see.
249
269
  */
250
270
  modelSelection?: boolean | undefined;
251
- /** SSE heartbeat interval. Defaults to 15 seconds. */
271
+ /**
272
+ * SSE heartbeat interval. Defaults to 5 seconds, under Bun.serve's default
273
+ * 10-second `idleTimeout`, which otherwise closes a quiet event stream.
274
+ */
252
275
  heartbeatMs?: number | undefined;
253
276
  };
254
277
  /**
@@ -6,11 +6,11 @@ import {
6
6
  downloadSessionProxySiteHtml,
7
7
  getSessionProxyArtifactAssociation,
8
8
  getSessionProxyWorkspaceGrant
9
- } from "./chunk-FYQAMXDW.js";
9
+ } from "./chunk-OLLQBZAE.js";
10
10
  import "./chunk-UM4ZXXJR.js";
11
11
  import "./chunk-U6CV2SXO.js";
12
12
  import "./chunk-APXVSSSY.js";
13
- import "./chunk-GYM54GKM.js";
13
+ import "./chunk-DTIMLEPB.js";
14
14
  import "./chunk-V6GIY62B.js";
15
15
  export {
16
16
  SESSION_PROXY_SITE_HTML_MAX_BYTES,
package/dist/site.js CHANGED
@@ -18,13 +18,13 @@ import {
18
18
  sanitizeOpenGeniSiteToolCallRequest,
19
19
  serveSiteHttp,
20
20
  siteSessionPath
21
- } from "./chunk-LEBDXYMA.js";
21
+ } from "./chunk-7A5ZDWQH.js";
22
22
  import "./chunk-4SMEOV3P.js";
23
- import "./chunk-BVIIRSJY.js";
23
+ import "./chunk-6SWFOZUP.js";
24
24
  import "./chunk-VPVMLM7Q.js";
25
25
  import "./chunk-APXVSSSY.js";
26
- import "./chunk-GYM54GKM.js";
27
- import "./chunk-NC57YMUO.js";
26
+ import "./chunk-DTIMLEPB.js";
27
+ import "./chunk-IJWKWFSF.js";
28
28
  import "./chunk-V6GIY62B.js";
29
29
  export {
30
30
  OPENGENI_SITE_BRIDGE_BOOTSTRAP_GLOBAL,
package/dist/types.d.ts CHANGED
@@ -3341,6 +3341,13 @@ export type ClientConfig = {
3341
3341
  modelSelection?: boolean | undefined;
3342
3342
  /** Session proxy sandbox-path download opt-in; absent on native deployments. */
3343
3343
  sandboxFiles?: boolean | undefined;
3344
+ /**
3345
+ * `false` when a host's session proxy turns live voice off
3346
+ * (`createSessionProxyHandler({ realtimeVoice: false })`), so UIs hide the
3347
+ * voice button. Otherwise absent: availability comes from the workspace's
3348
+ * realtime model catalog.
3349
+ */
3350
+ realtimeVoice?: boolean | undefined;
3344
3351
  /**
3345
3352
  * Session proxy only: the workspace the proxy resolved for this user, so a
3346
3353
  * browser pointed at the proxy (`<OpenGeniChat baseUrl=... />`) needs no
@@ -5948,6 +5955,14 @@ export type CapabilityRuntime = {
5948
5955
  state: "trusted" | "legacy_active" | "unverified";
5949
5956
  reason: "trusted_source" | "verified_probe" | "active_installation_compatibility" | "missing_verification";
5950
5957
  } | undefined;
5958
+ /**
5959
+ * Present when connecting needs an operator-registered OAuth client because
5960
+ * the provider refuses self-registration; `configured` is whether this
5961
+ * deployment has one. Connector surfaces offer the row only when true.
5962
+ */
5963
+ operatorOAuthClient?: {
5964
+ configured: boolean;
5965
+ } | undefined;
5951
5966
  };
5952
5967
  export type CapabilityCatalogItem = {
5953
5968
  id: string;
@@ -6695,6 +6710,8 @@ export type PromotionalCreditScope = {
6695
6710
  export type PromotionalCreditBalance = PromotionalCreditScope & {
6696
6711
  grantId: string;
6697
6712
  remainingMicros: number;
6713
+ /** Also pays for dictation and live voice, like general credits (signup credits). */
6714
+ coversVoice?: boolean | undefined;
6698
6715
  };
6699
6716
  export type BillingBalance = {
6700
6717
  accountId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/sdk",
3
- "version": "1.4.0",
3
+ "version": "1.4.4",
4
4
  "description": "Framework-agnostic TypeScript SDK for the OpenGeni API: typed client, session lifecycle, SSE event streaming with reconnect + replay-by-sequence, and proxy re-streaming helpers.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -182,8 +182,8 @@
182
182
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
183
183
  },
184
184
  "dependencies": {
185
- "@opengeni/connect": "1.4.0",
186
- "@opengeni/contracts": "1.4.0"
185
+ "@opengeni/connect": "1.4.4",
186
+ "@opengeni/contracts": "1.4.4"
187
187
  },
188
188
  "engines": {
189
189
  "node": ">=18"
@@ -31,6 +31,13 @@ export const DEFAULT_CHAT_SOURCE = "app";
31
31
 
32
32
  type SubmittedTurn = { after: number; turnId: string | null };
33
33
 
34
+ const MISSING_API_KEY_MESSAGE =
35
+ "Opengeni requires an apiKey. Set OPENGENI_API_KEY in the server environment.";
36
+
37
+ async function missingApiKeyFetch(): Promise<Response> {
38
+ throw new TypeError(MISSING_API_KEY_MESSAGE);
39
+ }
40
+
34
41
  type BuildCreate = (text: string, send: ChatSendOptions) => CreateSessionRequest;
35
42
  type SubmitCreate = (request: CreateSessionRequest) => Promise<CreateSessionResponse>;
36
43
 
@@ -74,12 +81,16 @@ export class OpenGeni {
74
81
  private pendingOrganizationId: Promise<string> | undefined;
75
82
 
76
83
  constructor(options: OpenGeniOptions) {
77
- if (!options.apiKey) throw new TypeError("Opengeni requires an apiKey.");
78
84
  this.client = new OpenGeniClient({
79
85
  // An empty value (a blank `OPENGENI_API_BASE_URL=` in .env) means the default.
80
86
  baseUrl: options.baseUrl?.trim() || DEFAULT_OPENGENI_BASE_URL,
81
- apiKey: options.apiKey,
82
- ...(options.fetch ? { fetch: options.fetch } : {}),
87
+ // A missing key fails each request, not construction: a module-scope
88
+ // `new OpenGeni({ apiKey: process.env.OPENGENI_API_KEY! })` must not
89
+ // break `next build` (or any import) where the secret only exists at
90
+ // runtime.
91
+ ...(options.apiKey
92
+ ? { apiKey: options.apiKey, ...(options.fetch ? { fetch: options.fetch } : {}) }
93
+ : { fetch: missingApiKeyFetch }),
83
94
  });
84
95
  this.organizationId = options.organizationId?.trim() ?? "";
85
96
  this.source = options.source ?? DEFAULT_CHAT_SOURCE;
@@ -142,15 +142,7 @@ const REALTIME_REFUSAL_CODES = new Set([
142
142
  export function codexRealtimeRefusal(error: unknown): CodexRealtimeRefusal | null {
143
143
  if (!(error instanceof OpenGeniApiError) || !error.code) return null;
144
144
  if (!REALTIME_REFUSAL_CODES.has(error.code)) return null;
145
- let message: string | null = null;
146
- try {
147
- const body = JSON.parse(error.body) as Record<string, unknown>;
148
- const nested =
149
- body.error && typeof body.error === "object" ? (body.error as Record<string, unknown>) : body;
150
- message = typeof nested.message === "string" && nested.message ? nested.message : null;
151
- } catch {
152
- message = null;
153
- }
145
+ const message = apiErrorMessage(error);
154
146
  return {
155
147
  code: error.code,
156
148
  message:
@@ -412,6 +404,9 @@ export function createCodexRealtimeController(
412
404
  let generation = 0;
413
405
  let reconnectAttempt = 0;
414
406
  let recoveryTerminal = false;
407
+ // Whether this mode ever reached a live provider connection. A definitive
408
+ // failure before that ends the mode instead of leaving an empty call open.
409
+ let connectedInMode = false;
415
410
  let mutationTail = Promise.resolve();
416
411
  let connectionTask: Promise<void> | null = null;
417
412
  const acceptedDelegationItemIds = new Set(
@@ -740,6 +735,12 @@ export function createCodexRealtimeController(
740
735
  if (closed || stopping || isAbortError(error)) return;
741
736
  const message = safeError(error);
742
737
  if (error instanceof CodexRealtimeMicrophoneError) {
738
+ if (error.code !== "track_ended" && !connectedInMode) {
739
+ // No conversation exists yet: end the call and say how to fix the
740
+ // microphone instead of holding an empty "reconnecting" call open.
741
+ await endAfterFailure(microphoneFailureMessage(error), null);
742
+ return;
743
+ }
743
744
  if (error.code === "track_ended" && state.mode?.state === "active") {
744
745
  reconnectAttempt += 1;
745
746
  publish({
@@ -783,6 +784,9 @@ export function createCodexRealtimeController(
783
784
  return;
784
785
  }
785
786
  }
787
+ } else if (error instanceof OpenGeniApiError && !error.retryable && !connectedInMode) {
788
+ await endAfterFailure(message, null);
789
+ return;
786
790
  } else if (error instanceof OpenGeniApiError && !error.retryable) {
787
791
  stopTimers();
788
792
  if (!active) releaseMicrophone();
@@ -1071,6 +1075,7 @@ export function createCodexRealtimeController(
1071
1075
  onFatal: (fatal) => onBridgeFatal(targetGeneration, bridge, fatal),
1072
1076
  });
1073
1077
  active = { generation: targetGeneration, transport: connected, bridge };
1078
+ connectedInMode = true;
1074
1079
  connected.setOutputMuted(state.outputMuted);
1075
1080
  recoveryTerminal = false;
1076
1081
  pendingAbort = null;
@@ -1190,7 +1195,13 @@ export function createCodexRealtimeController(
1190
1195
  * End the call because Opengeni refused it, keep final speech, and leave a
1191
1196
  * terminal, non-retrying state that explains why.
1192
1197
  */
1193
- const endForRefusal = async (refusal: CodexRealtimeRefusal): Promise<void> => {
1198
+ const endForRefusal = async (refusal: CodexRealtimeRefusal): Promise<void> =>
1199
+ await endAfterFailure(refusal.message, refusal);
1200
+
1201
+ const endAfterFailure = async (
1202
+ message: string,
1203
+ refusal: CodexRealtimeRefusal | null,
1204
+ ): Promise<void> => {
1194
1205
  try {
1195
1206
  await controller.stop();
1196
1207
  } catch {
@@ -1205,8 +1216,8 @@ export function createCodexRealtimeController(
1205
1216
  realtimeId: null,
1206
1217
  mode: null,
1207
1218
  bridge: null,
1208
- diagnostic: diagnostic("terminal_stop", refusal.message, false),
1209
- error: refusal.message,
1219
+ diagnostic: diagnostic("terminal_stop", message, false),
1220
+ error: message,
1210
1221
  refusal,
1211
1222
  });
1212
1223
  };
@@ -1263,6 +1274,7 @@ export function createCodexRealtimeController(
1263
1274
  closed = false;
1264
1275
  stopping = false;
1265
1276
  recoveryTerminal = false;
1277
+ connectedInMode = false;
1266
1278
  const record: OwnerRecord = {
1267
1279
  version: OWNER_RECORD_VERSION,
1268
1280
  workspaceId: options.workspaceId,
@@ -1303,6 +1315,8 @@ export function createCodexRealtimeController(
1303
1315
  if (recoveryTerminal && state.mode?.state === "active") return;
1304
1316
  const record = readOwnerRecord(storage, storageKey, options);
1305
1317
  if (!record) {
1318
+ // Keep a terminal failure readable after the server records the end.
1319
+ if (state.status === "error" && !state.mode) return;
1306
1320
  transitionEnded();
1307
1321
  return;
1308
1322
  }
@@ -1326,7 +1340,10 @@ export function createCodexRealtimeController(
1326
1340
  if (lifecycle.state === "ended") {
1327
1341
  const record = readOwnerRecord(storage, storageKey, options);
1328
1342
  if (record && record.operationId !== lifecycle.operationId) {
1329
- if (connectionTask || state.status === "active") return;
1343
+ // An earlier call's end is not news while this browser is starting
1344
+ // its own call (for example, waiting on the microphone prompt):
1345
+ // re-beginning would advance the lease and fail the pending start.
1346
+ if (connectionTask || state.status === "active" || state.status === "starting") return;
1330
1347
  closed = false;
1331
1348
  stopping = false;
1332
1349
  owner = record;
@@ -1342,6 +1359,9 @@ export function createCodexRealtimeController(
1342
1359
  await connectionTask;
1343
1360
  return;
1344
1361
  }
1362
+ // The server's end of a call that failed to start must not wipe the
1363
+ // failure the user still needs to read.
1364
+ if (state.status === "error" && !state.mode) return;
1345
1365
  if (state.realtimeId === null || lifecycle.realtimeId === state.realtimeId) {
1346
1366
  transitionEnded(`Realtime ended: ${lifecycle.reason}`);
1347
1367
  }
@@ -1801,6 +1821,37 @@ function isAbortError(error: unknown): boolean {
1801
1821
  return error instanceof Error && error.name === "AbortError";
1802
1822
  }
1803
1823
 
1824
+ function microphoneFailureMessage(error: CodexRealtimeMicrophoneError): string {
1825
+ switch (error.code) {
1826
+ case "permission_denied":
1827
+ return "Microphone access is blocked. Allow it in site settings, then try again.";
1828
+ case "device_not_found":
1829
+ return "No microphone was found. Connect one, then try again.";
1830
+ case "device_unavailable":
1831
+ return "Your microphone is busy or unavailable. Close other apps using it, then try again.";
1832
+ default:
1833
+ return /timed out/i.test(error.message)
1834
+ ? "Microphone access wasn't granted in time. Allow it when your browser asks, then try again."
1835
+ : "The microphone could not start. Try again.";
1836
+ }
1837
+ }
1838
+
1839
+ /** The server's own message, without the transport prefix or reference id. */
1840
+ function apiErrorMessage(error: OpenGeniApiError): string | null {
1841
+ try {
1842
+ const body = JSON.parse(error.body) as Record<string, unknown>;
1843
+ const nested =
1844
+ body.error && typeof body.error === "object" ? (body.error as Record<string, unknown>) : body;
1845
+ return typeof nested.message === "string" && nested.message ? nested.message : null;
1846
+ } catch {
1847
+ return null;
1848
+ }
1849
+ }
1850
+
1804
1851
  function safeError(error: unknown): string {
1805
- return error instanceof Error ? error.message : "Codex realtime browser controller failed";
1852
+ if (error instanceof OpenGeniApiError) {
1853
+ const message = apiErrorMessage(error);
1854
+ if (message) return /[.!?]$/.test(message) ? message : `${message}.`;
1855
+ }
1856
+ return error instanceof Error ? error.message : "Live voice failed in this browser.";
1806
1857
  }
@@ -1114,6 +1114,8 @@ export type ComputerSessionCapabilities = {
1114
1114
  screenCapture: boolean;
1115
1115
  semanticActions: boolean;
1116
1116
  pointerInput: boolean;
1117
+ /** Causal count-2 input requires its exact confirmed first operation. Absent means unsupported. */
1118
+ pointerClickContinuation?: boolean | undefined;
1117
1119
  keyboardInput: boolean;
1118
1120
  clipboard: boolean;
1119
1121
  backgroundActions: boolean;
@@ -1217,6 +1219,9 @@ export type ComputerAction =
1217
1219
  deltaX?: number | undefined;
1218
1220
  deltaY?: number | undefined;
1219
1221
  button?: "left" | "right" | "middle" | undefined;
1222
+ /** Click only; 2 delivers one second click pair after a completed first click. */
1223
+ clickCount?: 1 | 2 | undefined;
1224
+ continuationOfOperationId?: string | undefined;
1220
1225
  }
1221
1226
  | { type: "keyboard"; action: "type" | "press"; value: string }
1222
1227
  | {