@opengeni/sdk 1.4.0 → 1.4.2
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/README.md +12 -0
- package/dist/adapters/next.js +2 -2
- package/dist/artifacts.js +4 -4
- package/dist/browser.js +2 -2
- package/dist/chat/index.js +5 -5
- package/dist/{chunk-GYM54GKM.js → chunk-DTIMLEPB.js} +1 -1
- package/dist/chunk-DTIMLEPB.js.map +1 -0
- package/dist/{chunk-ZNC44QEX.js → chunk-JOI7XCG6.js} +45 -14
- package/dist/chunk-JOI7XCG6.js.map +1 -0
- package/dist/{chunk-LEBDXYMA.js → chunk-TRUFUTDI.js} +2 -2
- package/dist/{chunk-FYQAMXDW.js → chunk-TX6ED3AP.js} +108 -3
- package/dist/chunk-TX6ED3AP.js.map +1 -0
- package/dist/{chunk-D534PIXF.js → chunk-XIQIV4VL.js} +2 -2
- package/dist/{chunk-CA2T7QT4.js → chunk-XPWH3HKG.js} +2 -2
- package/dist/{chunk-MXV5O25O.js → chunk-YWLJGEUB.js} +2 -2
- package/dist/{chunk-BVIIRSJY.js → chunk-ZOSRUR2Q.js} +2 -2
- package/dist/codex-realtime-controller.js +1 -1
- package/dist/core.js +3 -3
- package/dist/document-authority.js +3 -3
- package/dist/editable-artifacts.js +1 -1
- package/dist/index.js +8 -8
- package/dist/realtime.js +1 -1
- package/dist/session-proxy.d.ts +29 -6
- package/dist/session-proxy.js +2 -2
- package/dist/site.js +3 -3
- package/dist/types.d.ts +17 -0
- package/package.json +3 -3
- package/src/codex-realtime-controller.ts +65 -14
- package/src/session-proxy.ts +184 -7
- package/src/types.ts +15 -0
- package/dist/chunk-FYQAMXDW.js.map +0 -1
- package/dist/chunk-GYM54GKM.js.map +0 -1
- package/dist/chunk-ZNC44QEX.js.map +0 -1
- /package/dist/{chunk-LEBDXYMA.js.map → chunk-TRUFUTDI.js.map} +0 -0
- /package/dist/{chunk-D534PIXF.js.map → chunk-XIQIV4VL.js.map} +0 -0
- /package/dist/{chunk-CA2T7QT4.js.map → chunk-XPWH3HKG.js.map} +0 -0
- /package/dist/{chunk-MXV5O25O.js.map → chunk-YWLJGEUB.js.map} +0 -0
- /package/dist/{chunk-BVIIRSJY.js.map → chunk-ZOSRUR2Q.js.map} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
OpenGeniDocumentAuthorityClient
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-XPWH3HKG.js";
|
|
4
|
+
import "./chunk-ZOSRUR2Q.js";
|
|
5
5
|
import "./chunk-VPVMLM7Q.js";
|
|
6
6
|
import "./chunk-APXVSSSY.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-DTIMLEPB.js";
|
|
8
8
|
import "./chunk-NC57YMUO.js";
|
|
9
9
|
import "./chunk-V6GIY62B.js";
|
|
10
10
|
export {
|
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-
|
|
5
|
+
} from "./chunk-XIQIV4VL.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-
|
|
39
|
-
import "./chunk-
|
|
40
|
-
import "./chunk-
|
|
38
|
+
} from "./chunk-JOI7XCG6.js";
|
|
39
|
+
import "./chunk-YWLJGEUB.js";
|
|
40
|
+
import "./chunk-XPWH3HKG.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-
|
|
70
|
+
} from "./chunk-TX6ED3AP.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-
|
|
95
|
+
} from "./chunk-TRUFUTDI.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-
|
|
115
|
+
} from "./chunk-ZOSRUR2Q.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-
|
|
136
|
+
} from "./chunk-DTIMLEPB.js";
|
|
137
137
|
import {
|
|
138
138
|
AttachedBrowserDeviceCollection,
|
|
139
139
|
AttachedBrowserDeviceResource,
|
package/dist/realtime.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
sessionRealtimeOwnerStorageKey,
|
|
24
24
|
sessionRealtimeOwnerStorageNamespace,
|
|
25
25
|
startCodexRealtimeWebrtc
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-JOI7XCG6.js";
|
|
27
27
|
import {
|
|
28
28
|
createGatewayRealtimeTransportStarter,
|
|
29
29
|
createXaiSubscriptionRealtimeTransportStarter
|
package/dist/session-proxy.d.ts
CHANGED
|
@@ -82,14 +82,21 @@ export type SessionProxyCreateInput = {
|
|
|
82
82
|
export type SessionProxyMessageInput = {
|
|
83
83
|
/** Absent for `create`. */
|
|
84
84
|
sessionId?: string | undefined;
|
|
85
|
-
|
|
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,
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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
|
-
/**
|
|
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
|
/**
|
package/dist/session-proxy.js
CHANGED
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
downloadSessionProxySiteHtml,
|
|
7
7
|
getSessionProxyArtifactAssociation,
|
|
8
8
|
getSessionProxyWorkspaceGrant
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-TX6ED3AP.js";
|
|
10
10
|
import "./chunk-UM4ZXXJR.js";
|
|
11
11
|
import "./chunk-U6CV2SXO.js";
|
|
12
12
|
import "./chunk-APXVSSSY.js";
|
|
13
|
-
import "./chunk-
|
|
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,12 +18,12 @@ import {
|
|
|
18
18
|
sanitizeOpenGeniSiteToolCallRequest,
|
|
19
19
|
serveSiteHttp,
|
|
20
20
|
siteSessionPath
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-TRUFUTDI.js";
|
|
22
22
|
import "./chunk-4SMEOV3P.js";
|
|
23
|
-
import "./chunk-
|
|
23
|
+
import "./chunk-ZOSRUR2Q.js";
|
|
24
24
|
import "./chunk-VPVMLM7Q.js";
|
|
25
25
|
import "./chunk-APXVSSSY.js";
|
|
26
|
-
import "./chunk-
|
|
26
|
+
import "./chunk-DTIMLEPB.js";
|
|
27
27
|
import "./chunk-NC57YMUO.js";
|
|
28
28
|
import "./chunk-V6GIY62B.js";
|
|
29
29
|
export {
|
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.
|
|
3
|
+
"version": "1.4.2",
|
|
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.
|
|
186
|
-
"@opengeni/contracts": "1.4.
|
|
185
|
+
"@opengeni/connect": "1.4.2",
|
|
186
|
+
"@opengeni/contracts": "1.4.2"
|
|
187
187
|
},
|
|
188
188
|
"engines": {
|
|
189
189
|
"node": ">=18"
|
|
@@ -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
|
-
|
|
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",
|
|
1209
|
-
error:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|