@lamplitisles/dsh-nanocodex 0.1.0-beta.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/LICENSE +65 -0
- package/NOTICE +2 -0
- package/README.md +182 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/cordis.patch.yml +52 -0
- package/dist/index.d.ts +435 -0
- package/dist/index.js +3405 -0
- package/dist/standard-preset.d.ts +7 -0
- package/dist/standard-preset.js +25 -0
- package/engine-release.json +14 -0
- package/package.json +119 -0
- package/presets/standard/agent.cordis.yml +6 -0
- package/presets/standard/preset.yml +2 -0
- package/vendor/nanocodex/README.md +1344 -0
- package/vendor/nanocodex/actions/events.d.mts +10 -0
- package/vendor/nanocodex/actions/events.mjs +163 -0
- package/vendor/nanocodex/actions/index.d.mts +13 -0
- package/vendor/nanocodex/actions/index.mjs +35 -0
- package/vendor/nanocodex/actions/session.d.mts +66 -0
- package/vendor/nanocodex/actions/session.mjs +59 -0
- package/vendor/nanocodex/actions/turn.d.mts +59 -0
- package/vendor/nanocodex/actions/turn.mjs +37 -0
- package/vendor/nanocodex/actions/voice.d.mts +22 -0
- package/vendor/nanocodex/actions/voice.mjs +50 -0
- package/vendor/nanocodex/browser/Agent.d.mts +55 -0
- package/vendor/nanocodex/browser/Agent.mjs +11 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.d.mts +7 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.mjs +19 -0
- package/vendor/nanocodex/browser/InlineAgent.mjs +266 -0
- package/vendor/nanocodex/browser/Transport.d.mts +74 -0
- package/vendor/nanocodex/browser/Transport.mjs +61 -0
- package/vendor/nanocodex/browser/Voice.d.mts +71 -0
- package/vendor/nanocodex/browser/Voice.mjs +291 -0
- package/vendor/nanocodex/browser/VoiceSession.mjs +688 -0
- package/vendor/nanocodex/browser/WorkerAgent.d.mts +65 -0
- package/vendor/nanocodex/browser/WorkerAgent.mjs +1472 -0
- package/vendor/nanocodex/browser/agent.worker.mjs +3 -0
- package/vendor/nanocodex/browser/config.d.mts +36 -0
- package/vendor/nanocodex/browser/config.mjs +439 -0
- package/vendor/nanocodex/browser/engine.mjs +13 -0
- package/vendor/nanocodex/browser/harness.mjs +55 -0
- package/vendor/nanocodex/browser/host.d.mts +83 -0
- package/vendor/nanocodex/browser/host.mjs +555 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.d.mts +14 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.mjs +110 -0
- package/vendor/nanocodex/browser/index.d.mts +92 -0
- package/vendor/nanocodex/browser/index.mjs +23 -0
- package/vendor/nanocodex/browser/indexeddb-durability-store.mjs +197 -0
- package/vendor/nanocodex/browser/workspace.d.mts +16 -0
- package/vendor/nanocodex/browser/workspace.mjs +107 -0
- package/vendor/nanocodex/cloud/Client.d.mts +83 -0
- package/vendor/nanocodex/cloud/Client.mjs +231 -0
- package/vendor/nanocodex/cloud/Decorator.d.mts +31 -0
- package/vendor/nanocodex/cloud/Decorator.mjs +37 -0
- package/vendor/nanocodex/cloud/Dialog.d.mts +113 -0
- package/vendor/nanocodex/cloud/Dialog.mjs +366 -0
- package/vendor/nanocodex/cloud/Errors.d.mts +22 -0
- package/vendor/nanocodex/cloud/Errors.mjs +39 -0
- package/vendor/nanocodex/cloud/Principal.d.mts +26 -0
- package/vendor/nanocodex/cloud/Principal.mjs +88 -0
- package/vendor/nanocodex/cloud/RemoteProvider.mjs +88 -0
- package/vendor/nanocodex/cloud/Transport.d.mts +47 -0
- package/vendor/nanocodex/cloud/Transport.mjs +454 -0
- package/vendor/nanocodex/cloud/actions/account.d.mts +9 -0
- package/vendor/nanocodex/cloud/actions/account.mjs +19 -0
- package/vendor/nanocodex/cloud/actions/agent.d.mts +18 -0
- package/vendor/nanocodex/cloud/actions/agent.mjs +428 -0
- package/vendor/nanocodex/cloud/actions/connection.d.mts +133 -0
- package/vendor/nanocodex/cloud/actions/connection.mjs +580 -0
- package/vendor/nanocodex/cloud/actions/grant.d.mts +10 -0
- package/vendor/nanocodex/cloud/actions/grant.mjs +12 -0
- package/vendor/nanocodex/cloud/actions/index.d.mts +7 -0
- package/vendor/nanocodex/cloud/actions/index.mjs +7 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.d.mts +23 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.mjs +37 -0
- package/vendor/nanocodex/cloud/actions/model.d.mts +12 -0
- package/vendor/nanocodex/cloud/actions/model.mjs +50 -0
- package/vendor/nanocodex/cloud/actions/mpp.d.mts +28 -0
- package/vendor/nanocodex/cloud/actions/mpp.mjs +39 -0
- package/vendor/nanocodex/cloud/index.d.mts +30 -0
- package/vendor/nanocodex/cloud/index.mjs +9 -0
- package/vendor/nanocodex/cloud/internal.mjs +533 -0
- package/vendor/nanocodex/cloud/mercator.mjs +108 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.d.mts +34 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.mjs +202 -0
- package/vendor/nanocodex/cloud/server/index.d.mts +1 -0
- package/vendor/nanocodex/cloud/server/index.mjs +1 -0
- package/vendor/nanocodex/cloud/types.d.mts +211 -0
- package/vendor/nanocodex/cloudflare/Agent.d.mts +133 -0
- package/vendor/nanocodex/cloudflare/Agent.mjs +811 -0
- package/vendor/nanocodex/cloudflare/egress-subject.mjs +19 -0
- package/vendor/nanocodex/cloudflare/egress.d.mts +41 -0
- package/vendor/nanocodex/cloudflare/egress.mjs +119 -0
- package/vendor/nanocodex/cloudflare/event-socket.mjs +316 -0
- package/vendor/nanocodex/cloudflare/index.d.mts +7 -0
- package/vendor/nanocodex/cloudflare/index.mjs +6 -0
- package/vendor/nanocodex/host/Agent.d.mts +55 -0
- package/vendor/nanocodex/host/Agent.mjs +1 -0
- package/vendor/nanocodex/host/index.d.mts +82 -0
- package/vendor/nanocodex/host/index.mjs +10 -0
- package/vendor/nanocodex/index.d.mts +81 -0
- package/vendor/nanocodex/index.mjs +12 -0
- package/vendor/nanocodex/internal.mjs +1275 -0
- package/vendor/nanocodex/managed/Agent.d.mts +380 -0
- package/vendor/nanocodex/managed/Agent.mjs +1694 -0
- package/vendor/nanocodex/managed/ManagedError.d.mts +9 -0
- package/vendor/nanocodex/managed/ManagedError.mjs +8 -0
- package/vendor/nanocodex/managed/README.md +241 -0
- package/vendor/nanocodex/managed/Voice.mjs +202 -0
- package/vendor/nanocodex/managed/index.d.mts +20 -0
- package/vendor/nanocodex/managed/index.mjs +2 -0
- package/vendor/nanocodex/managed/internal.mjs +119 -0
- package/vendor/nanocodex/node/Agent.d.mts +50 -0
- package/vendor/nanocodex/node/Agent.mjs +203 -0
- package/vendor/nanocodex/node/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/node/ChatGptSubscription.mjs +17 -0
- package/vendor/nanocodex/node/Transport.d.mts +44 -0
- package/vendor/nanocodex/node/Transport.mjs +46 -0
- package/vendor/nanocodex/node/host.mjs +731 -0
- package/vendor/nanocodex/node/index.d.mts +73 -0
- package/vendor/nanocodex/node/index.mjs +17 -0
- package/vendor/nanocodex/node/workspace.d.mts +10 -0
- package/vendor/nanocodex/node/workspace.mjs +198 -0
- package/vendor/nanocodex/package.json +200 -0
- package/vendor/nanocodex/pkg-node/nanocodex.d.ts +675 -0
- package/vendor/nanocodex/pkg-node/nanocodex.js +2628 -0
- package/vendor/nanocodex/pkg-node/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-node/package.json +1 -0
- package/vendor/nanocodex/pkg-web/.nanocodex-bindgen-stamp +6 -0
- package/vendor/nanocodex/pkg-web/nanocodex-build.json +1 -0
- package/vendor/nanocodex/pkg-web/nanocodex.d.ts +805 -0
- package/vendor/nanocodex/pkg-web/nanocodex.js +2714 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.js +2615 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm +0 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-web/nanocodex_worker.js +9 -0
- package/vendor/nanocodex/pkg-web/package.json +1 -0
- package/vendor/nanocodex/runtime/chatgpt-subscription.mjs +212 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.d.mts +26 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.mjs +168 -0
- package/vendor/nanocodex/runtime/code-evaluator.worker.mjs +137 -0
- package/vendor/nanocodex/runtime/code-runtime.mjs +1 -0
- package/vendor/nanocodex/runtime/durability-store.d.mts +85 -0
- package/vendor/nanocodex/runtime/durability-store.mjs +631 -0
- package/vendor/nanocodex/runtime/durability.mjs +153 -0
- package/vendor/nanocodex/runtime/managed-transport.mjs +460 -0
- package/vendor/nanocodex/runtime/mcp-runtime.mjs +534 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.d.mts +64 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.mjs +532 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.d.mts +17 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.mjs +236 -0
- package/vendor/nanocodex/runtime/responses-transport.mjs +22 -0
- package/vendor/nanocodex/runtime/subagents.d.mts +107 -0
- package/vendor/nanocodex/runtime/subagents.mjs +54 -0
- package/vendor/nanocodex/runtime/subscription-store.d.mts +8 -0
- package/vendor/nanocodex/runtime/subscription-store.mjs +42 -0
- package/vendor/nanocodex/runtime/tempo-provider.d.mts +125 -0
- package/vendor/nanocodex/runtime/tempo-provider.mjs +264 -0
- package/vendor/nanocodex/runtime/tool-configuration.mjs +1 -0
- package/vendor/nanocodex/runtime/tool-router.mjs +1 -0
- package/vendor/nanocodex/runtime/utf8.mjs +1 -0
- package/vendor/nanocodex/runtime/worker-evaluator.mjs +162 -0
- package/vendor/nanocodex/runtime/workspace.d.mts +1 -0
- package/vendor/nanocodex/runtime/workspace.mjs +1 -0
- package/vendor/nanocodex/tools/Tools.d.mts +56 -0
- package/vendor/nanocodex/tools/Tools.mjs +136 -0
- package/vendor/nanocodex/tools/artifact.d.mts +1 -0
- package/vendor/nanocodex/tools/artifact.mjs +1 -0
- package/vendor/nanocodex/tools/attachment.mjs +1 -0
- package/vendor/nanocodex/tools/bash.d.mts +1 -0
- package/vendor/nanocodex/tools/bash.mjs +1 -0
- package/vendor/nanocodex/tools/browser/accountInfo.mjs +795 -0
- package/vendor/nanocodex/tools/browser/browserBuffer.mjs +18 -0
- package/vendor/nanocodex/tools/browser/browserCompiler.mjs +155 -0
- package/vendor/nanocodex/tools/browser/browserEgress.mjs +161 -0
- package/vendor/nanocodex/tools/browser/browserPython.mjs +133 -0
- package/vendor/nanocodex/tools/browser/browserShell.mjs +1075 -0
- package/vendor/nanocodex/tools/browser/browserSprintf.mjs +4 -0
- package/vendor/nanocodex/tools/browser/browserSsh.mjs +201 -0
- package/vendor/nanocodex/tools/browser/browserZlib.mjs +136 -0
- package/vendor/nanocodex/tools/browser/compiler.worker.mjs +160 -0
- package/vendor/nanocodex/tools/browser/devTunnelsSshBrowser.mjs +13 -0
- package/vendor/nanocodex/tools/browser/index.d.mts +209 -0
- package/vendor/nanocodex/tools/browser/index.mjs +174 -0
- package/vendor/nanocodex/tools/browser/opfsGit.mjs +248 -0
- package/vendor/nanocodex/tools/browser/python.worker.mjs +114 -0
- package/vendor/nanocodex/tools/browser/threadGit.mjs +197 -0
- package/vendor/nanocodex/tools/browser/unsupportedNodeRsa.mjs +5 -0
- package/vendor/nanocodex/tools/browser/workspace.mjs +83 -0
- package/vendor/nanocodex/tools/dataset.d.mts +1 -0
- package/vendor/nanocodex/tools/dataset.mjs +1 -0
- package/vendor/nanocodex/tools/datasetContract.mjs +1 -0
- package/vendor/nanocodex/tools/datasetEngine.mjs +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.d.mts +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.mjs +1 -0
- package/vendor/nanocodex/tools/index.d.mts +49 -0
- package/vendor/nanocodex/tools/index.mjs +19 -0
- package/vendor/nanocodex/tools/namedTool.mjs +1 -0
- package/vendor/nanocodex/tools/repository-workspace.d.mts +1 -0
- package/vendor/nanocodex/tools/repository-workspace.mjs +1 -0
- package/vendor/nanocodex/tools/ssh.d.mts +1 -0
- package/vendor/nanocodex/tools/ssh.mjs +1 -0
- package/vendor/nanocodex/tools/standard.mjs +1 -0
- package/vendor/nanocodex/tools/standardDescriptions.mjs +1 -0
- package/vendor/nanocodex/types.d.mts +663 -0
- package/vendor/nanocodex/wasm.d.mts +4 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.mjs +29 -0
- package/vendor/nanocodex/worker/index.d.mts +18 -0
- package/vendor/nanocodex/worker/index.mjs +5 -0
- package/vendor/nanocodex-tools/README.md +65 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.d.ts +7 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.js +14 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.d.ts +172 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.js +1278 -0
- package/vendor/nanocodex-tools/dist/hosted/index.d.ts +3 -0
- package/vendor/nanocodex-tools/dist/hosted/index.js +3 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.d.ts +120 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.js +415 -0
- package/vendor/nanocodex-tools/dist/index.d.ts +19 -0
- package/vendor/nanocodex-tools/dist/index.js +14 -0
- package/vendor/nanocodex-tools/dist/memory.d.ts +204 -0
- package/vendor/nanocodex-tools/dist/memory.js +411 -0
- package/vendor/nanocodex-tools/dist/namespace.d.ts +87 -0
- package/vendor/nanocodex-tools/dist/namespace.js +338 -0
- package/vendor/nanocodex-tools/dist/repository-archive.d.ts +4 -0
- package/vendor/nanocodex-tools/dist/repository-archive.js +104 -0
- package/vendor/nanocodex-tools/dist/runtime.d.ts +30 -0
- package/vendor/nanocodex-tools/dist/runtime.js +37 -0
- package/vendor/nanocodex-tools/dist/session.d.ts +167 -0
- package/vendor/nanocodex-tools/dist/session.js +286 -0
- package/vendor/nanocodex-tools/dist/shell.d.ts +51 -0
- package/vendor/nanocodex-tools/dist/shell.js +553 -0
- package/vendor/nanocodex-tools/dist/ssh.d.ts +29 -0
- package/vendor/nanocodex-tools/dist/ssh.js +66 -0
- package/vendor/nanocodex-tools/dist/workspace.d.ts +45 -0
- package/vendor/nanocodex-tools/dist/workspace.js +237 -0
- package/vendor/nanocodex-tools/package.json +122 -0
- package/vendor/nanocodex-tools/runtime/code-runtime.mjs +817 -0
- package/vendor/nanocodex-tools/runtime/tool-configuration.mjs +50 -0
- package/vendor/nanocodex-tools/runtime/tool-router.mjs +730 -0
- package/vendor/nanocodex-tools/runtime/utf8.d.mts +2 -0
- package/vendor/nanocodex-tools/runtime/utf8.mjs +25 -0
- package/vendor/nanocodex-tools/tools/artifact.d.mts +69 -0
- package/vendor/nanocodex-tools/tools/artifact.mjs +270 -0
- package/vendor/nanocodex-tools/tools/attachment.mjs +603 -0
- package/vendor/nanocodex-tools/tools/bash.d.mts +90 -0
- package/vendor/nanocodex-tools/tools/bash.mjs +742 -0
- package/vendor/nanocodex-tools/tools/dataset.d.mts +9 -0
- package/vendor/nanocodex-tools/tools/dataset.mjs +47 -0
- package/vendor/nanocodex-tools/tools/datasetContract.mjs +54 -0
- package/vendor/nanocodex-tools/tools/datasetEngine.mjs +1167 -0
- package/vendor/nanocodex-tools/tools/execution-contract.d.mts +5 -0
- package/vendor/nanocodex-tools/tools/execution-contract.mjs +63 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.d.mts +20 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.mjs +67 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.mjs +56 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.d.mts +1 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.mjs +1 -0
- package/vendor/nanocodex-tools/tools/namedTool.d.mts +3 -0
- package/vendor/nanocodex-tools/tools/namedTool.mjs +3 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.mjs +314 -0
- package/vendor/nanocodex-tools/tools/processOutput.mjs +56 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.d.mts +55 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.mjs +397 -0
- package/vendor/nanocodex-tools/tools/ssh.d.mts +56 -0
- package/vendor/nanocodex-tools/tools/ssh.mjs +503 -0
- package/vendor/nanocodex-tools/tools/standard.mjs +458 -0
- package/vendor/nanocodex-tools/tools/standardDescriptions.mjs +3 -0
- package/vendor/nanocodex-tools/tools/types.d.mts +83 -0
- package/vendor/nanocodex-tools/tools/workspace.d.mts +25 -0
- package/vendor/nanocodex-tools/tools/workspace.mjs +282 -0
- package/vendor/nanocodex-tools/tools/x.d.mts +30 -0
- package/vendor/nanocodex-tools/tools/x.mjs +91 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
export const MICROPHONE_CAPTURE_TIMEOUT_MS = 15_000;
|
|
2
|
+
export const ICE_GATHERING_TIMEOUT_MS = 15_000;
|
|
3
|
+
export const REALTIME_CALL_TIMEOUT_MS = 15_000;
|
|
4
|
+
export const SIDEBAND_OPEN_TIMEOUT_MS = 15_000;
|
|
5
|
+
|
|
6
|
+
export class VoiceError extends Error {
|
|
7
|
+
constructor(code, message, options = {}) {
|
|
8
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
9
|
+
this.name = "VoiceError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Owns browser speaker playback and retries it from the next user gesture when autoplay is blocked. */
|
|
15
|
+
export class SpeakerPlayback {
|
|
16
|
+
#speaker;
|
|
17
|
+
#gestures;
|
|
18
|
+
#onStatus;
|
|
19
|
+
#resume;
|
|
20
|
+
#closed = false;
|
|
21
|
+
|
|
22
|
+
constructor(speaker, onStatus, gestures = document) {
|
|
23
|
+
this.#speaker = speaker;
|
|
24
|
+
this.#onStatus = onStatus;
|
|
25
|
+
this.#gestures = gestures;
|
|
26
|
+
this.#speaker.autoplay = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
attach(stream) {
|
|
30
|
+
if (this.#closed) return;
|
|
31
|
+
this.#speaker.srcObject = stream;
|
|
32
|
+
this.#play();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setEnabled(enabled) {
|
|
36
|
+
this.#speaker.muted = !enabled;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
close() {
|
|
40
|
+
if (this.#closed) return;
|
|
41
|
+
this.#closed = true;
|
|
42
|
+
this.#disarm();
|
|
43
|
+
this.#speaker.pause();
|
|
44
|
+
this.#speaker.srcObject = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#play() {
|
|
48
|
+
if (this.#closed) return;
|
|
49
|
+
this.#disarm();
|
|
50
|
+
void this.#speaker.play().catch(() => {
|
|
51
|
+
if (this.#closed) return;
|
|
52
|
+
this.#onStatus("Voice connected — tap once to enable speaker audio");
|
|
53
|
+
const resume = () => {
|
|
54
|
+
if (this.#resume !== resume) return;
|
|
55
|
+
this.#resume = undefined;
|
|
56
|
+
this.#gestures.removeEventListener("click", resume, true);
|
|
57
|
+
this.#play();
|
|
58
|
+
};
|
|
59
|
+
this.#resume = resume;
|
|
60
|
+
this.#gestures.addEventListener("click", resume, { capture: true, once: true });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#disarm() {
|
|
65
|
+
if (!this.#resume) return;
|
|
66
|
+
this.#gestures.removeEventListener("click", this.#resume, true);
|
|
67
|
+
this.#resume = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Executes browser-only media and network effects for the Rust-owned voice controller. */
|
|
72
|
+
export class BrowserVoiceSession {
|
|
73
|
+
#options;
|
|
74
|
+
#core;
|
|
75
|
+
#admission;
|
|
76
|
+
#peer;
|
|
77
|
+
#channel;
|
|
78
|
+
#sideband;
|
|
79
|
+
#sidebandUrl;
|
|
80
|
+
#sidebandCallId;
|
|
81
|
+
#sidebandOpenedAt;
|
|
82
|
+
#sidebandGeneration = 0;
|
|
83
|
+
#microphone;
|
|
84
|
+
#speaker;
|
|
85
|
+
#playbackEnabled = false;
|
|
86
|
+
#call;
|
|
87
|
+
#flushTimer;
|
|
88
|
+
#reconnectTimer;
|
|
89
|
+
#inbound = Promise.resolve();
|
|
90
|
+
#liveUpdates = new Set();
|
|
91
|
+
#starting;
|
|
92
|
+
#closePromise;
|
|
93
|
+
#closed = false;
|
|
94
|
+
#closing = new AbortController();
|
|
95
|
+
|
|
96
|
+
constructor(options) {
|
|
97
|
+
this.#options = options;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
start() {
|
|
101
|
+
this.#starting ??= this.#start();
|
|
102
|
+
return this.#starting;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async #start() {
|
|
106
|
+
if (!this.#options.captureMicrophone && !navigator.mediaDevices?.getUserMedia) {
|
|
107
|
+
throw new Error("this browser does not expose microphone capture");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// This call intentionally precedes every await so mobile user activation is retained.
|
|
111
|
+
const capture = this.#options.captureMicrophone?.() ?? capturePreferredMicrophone(
|
|
112
|
+
async (current, labels) => {
|
|
113
|
+
const core = await this.#options.core;
|
|
114
|
+
return core.preferredPhysicalInput(current, JSON.stringify(labels));
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
const microphoneCapture = acquireMicrophone(capture, this.#closing.signal).then((microphone) => {
|
|
118
|
+
if (this.#closed || this.#closing.signal.aborted) stopStream(microphone);
|
|
119
|
+
else this.#microphone = microphone;
|
|
120
|
+
return microphone;
|
|
121
|
+
});
|
|
122
|
+
const coreReady = Promise.resolve(this.#options.core).then(async (core) => {
|
|
123
|
+
if (this.#closed || this.#closing.signal.aborted) { core.free(); return; }
|
|
124
|
+
this.#core = core;
|
|
125
|
+
if (this.#options.settings) await core.configure(JSON.stringify(this.#options.settings));
|
|
126
|
+
await this.#options.beforeAgentTurn?.();
|
|
127
|
+
if (this.#closed || this.#closing.signal.aborted) return;
|
|
128
|
+
return core;
|
|
129
|
+
});
|
|
130
|
+
// Managed Rust can deliver authoritative context after SDP negotiation.
|
|
131
|
+
// Start both immediately, but fence incoming control events on admission.
|
|
132
|
+
const coreStartup = coreReady.then(async (core) => {
|
|
133
|
+
await core?.start();
|
|
134
|
+
return core;
|
|
135
|
+
});
|
|
136
|
+
this.#admission = coreStartup;
|
|
137
|
+
const connection = this.#connect(coreReady, coreStartup, microphoneCapture);
|
|
138
|
+
try {
|
|
139
|
+
await Promise.all([coreStartup, connection]);
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
this.#closing.abort();
|
|
142
|
+
this.#stopBrowserIo();
|
|
143
|
+
// Neither late admission nor negotiation can revive disposed resources.
|
|
144
|
+
await Promise.allSettled([coreStartup, connection]);
|
|
145
|
+
if (this.#closed) return;
|
|
146
|
+
throw cause;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async #connect(coreReady, coreStartup, microphoneCapture) {
|
|
151
|
+
const [core, media] = await Promise.all([
|
|
152
|
+
coreReady.then((core) => core?.parallelStartup ? core : coreStartup),
|
|
153
|
+
this.#prepareMedia(microphoneCapture),
|
|
154
|
+
]);
|
|
155
|
+
if (this.#closed || this.#closing.signal.aborted || !core || !media) return;
|
|
156
|
+
const { peer, sdp } = media;
|
|
157
|
+
|
|
158
|
+
const call = new AbortController();
|
|
159
|
+
this.#call = call;
|
|
160
|
+
const body = await core.callBody(sdp);
|
|
161
|
+
if (this.#closed) return;
|
|
162
|
+
let callResponse;
|
|
163
|
+
try {
|
|
164
|
+
callResponse = await withStartupDeadline(async () => {
|
|
165
|
+
const response = this.#options.call
|
|
166
|
+
? await this.#options.call(body, call.signal)
|
|
167
|
+
: await fetch(this.#options.callUrl ?? "/api/realtime/calls", {
|
|
168
|
+
method: "POST",
|
|
169
|
+
signal: call.signal,
|
|
170
|
+
credentials: "same-origin",
|
|
171
|
+
headers: {
|
|
172
|
+
"content-type": "application/json",
|
|
173
|
+
"x-nanocodex-request": "1",
|
|
174
|
+
},
|
|
175
|
+
body,
|
|
176
|
+
});
|
|
177
|
+
if (!response.ok) throw new Error(await responseError(response, "voice connection failed"));
|
|
178
|
+
const location = response.headers.get("x-nanocodex-realtime-location");
|
|
179
|
+
if (!location) throw new Error("voice connection did not return a Realtime Location");
|
|
180
|
+
return { location, body: await response.text() };
|
|
181
|
+
}, {
|
|
182
|
+
signal: call.signal,
|
|
183
|
+
timeoutMs: REALTIME_CALL_TIMEOUT_MS,
|
|
184
|
+
onTimeout: () => call.abort(),
|
|
185
|
+
timeoutError: new VoiceError(
|
|
186
|
+
"realtime_call_timeout",
|
|
187
|
+
"The Realtime voice connection request did not finish in time. Check your network connection, then retry.",
|
|
188
|
+
),
|
|
189
|
+
});
|
|
190
|
+
} catch (cause) {
|
|
191
|
+
if (this.#closed) return;
|
|
192
|
+
this.#stopBrowserIo();
|
|
193
|
+
throw cause;
|
|
194
|
+
}
|
|
195
|
+
const completed = JSON.parse(await core.completeCall(callResponse.body, callResponse.location));
|
|
196
|
+
if (this.#closed || peer.signalingState === "closed") return;
|
|
197
|
+
this.#sidebandCallId = completed.call_id;
|
|
198
|
+
this.#sidebandUrl = this.#options.sidebandUrl
|
|
199
|
+
? undefined
|
|
200
|
+
: String(await core.sidebandUrl(completed.call_id));
|
|
201
|
+
if (this.#closed) return;
|
|
202
|
+
try {
|
|
203
|
+
await Promise.all([
|
|
204
|
+
peer.setRemoteDescription({ type: "answer", sdp: completed.sdp }),
|
|
205
|
+
this.#openSideband(),
|
|
206
|
+
]);
|
|
207
|
+
} catch (cause) {
|
|
208
|
+
if (this.#closed) return;
|
|
209
|
+
this.#stopBrowserIo();
|
|
210
|
+
throw cause;
|
|
211
|
+
}
|
|
212
|
+
if (this.#closed) return;
|
|
213
|
+
this.#status(`Voice active (${this.#options.voice}) — /voice off to stop`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async #prepareMedia(capture) {
|
|
217
|
+
const microphone = await capture;
|
|
218
|
+
if (this.#closed || this.#closing.signal.aborted) {
|
|
219
|
+
stopStream(microphone);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const track of microphone.getAudioTracks()) {
|
|
223
|
+
track.contentHint = "speech";
|
|
224
|
+
track.addEventListener("mute", () => this.#status("Voice paused — microphone interrupted"));
|
|
225
|
+
track.addEventListener("unmute", () => this.#status(`Voice active (${this.#options.voice})`));
|
|
226
|
+
track.addEventListener("ended", () => {
|
|
227
|
+
this.#options.onTerminated("Voice microphone ended — tap Voice to reconnect");
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const peer = new RTCPeerConnection();
|
|
232
|
+
this.#peer = peer;
|
|
233
|
+
for (const track of microphone.getAudioTracks()) peer.addTrack(track, microphone);
|
|
234
|
+
this.#channel = peer.createDataChannel("oai-events");
|
|
235
|
+
peer.addEventListener("track", (event) => {
|
|
236
|
+
if (this.#closed || this.#closing.signal.aborted || this.#peer !== peer) {
|
|
237
|
+
event.track.stop();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const stream = event.streams[0] ?? new MediaStream([event.track]);
|
|
241
|
+
this.#speaker ??= new SpeakerPlayback(new Audio(), this.#options.onStatus);
|
|
242
|
+
this.#speaker.setEnabled(this.#playbackEnabled);
|
|
243
|
+
this.#speaker.attach(stream);
|
|
244
|
+
});
|
|
245
|
+
peer.addEventListener("connectionstatechange", () => {
|
|
246
|
+
if (this.#closed || this.#closing.signal.aborted || this.#peer !== peer) return;
|
|
247
|
+
if (peer.connectionState === "failed" || peer.connectionState === "disconnected") {
|
|
248
|
+
this.#options.onTerminated(`Voice ${peer.connectionState} — tap Voice to reconnect`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const offer = await peer.createOffer();
|
|
253
|
+
if (this.#closed || this.#closing.signal.aborted) return;
|
|
254
|
+
await peer.setLocalDescription(offer);
|
|
255
|
+
if (this.#closed || peer.signalingState === "closed") return;
|
|
256
|
+
// The server supplies candidates in its answer; gather local candidates
|
|
257
|
+
// while that request is in flight instead of waiting for every interface.
|
|
258
|
+
const sdp = offer.sdp;
|
|
259
|
+
if (!sdp) throw new Error("the browser did not produce a Realtime WebRTC offer");
|
|
260
|
+
|
|
261
|
+
return { peer, sdp };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
observe(envelope) {
|
|
265
|
+
if (!this.#closed && this.#core) {
|
|
266
|
+
this.#applyLive(() => this.#core.agentEvent(JSON.stringify(envelope)));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
command(method, ...args) {
|
|
271
|
+
if (this.#closed || !this.#core) return Promise.reject(new Error("voice is not active"));
|
|
272
|
+
const next = this.#inbound.then(() => {
|
|
273
|
+
if (this.#closed || !this.#core) throw new Error("voice is not active");
|
|
274
|
+
return this.#core[method](...args);
|
|
275
|
+
}).then((effects) => this.#apply(effects));
|
|
276
|
+
// Invalid app input rejects the command without ending an otherwise healthy call.
|
|
277
|
+
this.#inbound = next.catch(() => {});
|
|
278
|
+
return next;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
cancel() {
|
|
282
|
+
return this.#core?.cancel() ?? Promise.resolve(false);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
close() {
|
|
286
|
+
if (this.#closePromise) return this.#closePromise;
|
|
287
|
+
this.#closed = true;
|
|
288
|
+
this.#closing.abort();
|
|
289
|
+
// Microphone and speaker ownership ends synchronously. Protocol tail/lifecycle
|
|
290
|
+
// cleanup may legitimately wait behind an independent coding turn.
|
|
291
|
+
this.#stopBrowserMedia();
|
|
292
|
+
this.#closePromise = this.#finishClose();
|
|
293
|
+
return this.#closePromise;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
abort() {
|
|
297
|
+
if (this.#closed && this.#closePromise) return this.#closePromise.catch(() => {});
|
|
298
|
+
this.#closed = true;
|
|
299
|
+
this.#closing.abort();
|
|
300
|
+
this.#stopBrowserIo();
|
|
301
|
+
this.#closePromise = (async () => {
|
|
302
|
+
await this.#starting?.catch(() => {});
|
|
303
|
+
await Promise.all(this.#liveUpdates);
|
|
304
|
+
await this.#inbound;
|
|
305
|
+
this.#core?.free();
|
|
306
|
+
this.#core = undefined;
|
|
307
|
+
})();
|
|
308
|
+
return this.#closePromise;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async #finishClose() {
|
|
312
|
+
try {
|
|
313
|
+
await this.#starting?.catch(() => {});
|
|
314
|
+
await Promise.all(this.#liveUpdates);
|
|
315
|
+
await this.#inbound;
|
|
316
|
+
if (this.#core) {
|
|
317
|
+
await this.#options.beforeAgentTurn?.();
|
|
318
|
+
await this.#apply(await this.#core.stop());
|
|
319
|
+
}
|
|
320
|
+
} finally {
|
|
321
|
+
this.#stopBrowserIo();
|
|
322
|
+
this.#core?.free();
|
|
323
|
+
this.#core = undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#enqueue(operation, accepted = false) {
|
|
328
|
+
if (this.#closed && !accepted) return Promise.resolve();
|
|
329
|
+
const next = this.#inbound.then(operation).then((effects) => this.#apply(effects));
|
|
330
|
+
this.#inbound = next.catch((error) => {
|
|
331
|
+
if (!this.#closed) this.#options.onTerminated(errorMessage(error));
|
|
332
|
+
});
|
|
333
|
+
return next;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#applyLive(operation) {
|
|
337
|
+
if (this.#closed) return;
|
|
338
|
+
const next = Promise.resolve().then(operation).then((effects) => this.#apply(effects))
|
|
339
|
+
.catch((error) => {
|
|
340
|
+
if (!this.#closed) this.#options.onTerminated(errorMessage(error));
|
|
341
|
+
}).finally(() => this.#liveUpdates.delete(next));
|
|
342
|
+
this.#liveUpdates.add(next);
|
|
343
|
+
return next;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async #apply(encoded) {
|
|
347
|
+
const effects = typeof encoded === "string" ? JSON.parse(encoded) : encoded;
|
|
348
|
+
if (!effects || typeof effects !== "object") return;
|
|
349
|
+
if (effects.playback_enabled === false) {
|
|
350
|
+
this.#playbackEnabled = false;
|
|
351
|
+
this.#speaker?.setEnabled(false);
|
|
352
|
+
}
|
|
353
|
+
let sent = 0;
|
|
354
|
+
for (const frame of effects.frames ?? []) {
|
|
355
|
+
if (this.#sideband?.readyState === WebSocket.OPEN) {
|
|
356
|
+
this.#sideband.send(frame);
|
|
357
|
+
sent += 1;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (effects.acknowledge_frames && sent > 0) await this.#core?.framesSent(sent);
|
|
361
|
+
if (effects.playback_enabled === true && sent === (effects.frames?.length ?? 0)) {
|
|
362
|
+
this.#playbackEnabled = true;
|
|
363
|
+
this.#speaker?.setEnabled(true);
|
|
364
|
+
}
|
|
365
|
+
for (const entry of effects.transcripts ?? []) {
|
|
366
|
+
this.#options.onTranscript(entry.speaker, entry.text, entry);
|
|
367
|
+
}
|
|
368
|
+
if (effects.status) this.#status(effects.status);
|
|
369
|
+
if (effects.schedule_flush && this.#flushTimer === undefined && !this.#closed) {
|
|
370
|
+
this.#flushTimer = window.setTimeout(() => {
|
|
371
|
+
this.#flushTimer = undefined;
|
|
372
|
+
if (this.#core && !this.#closed) this.#applyLive(() => this.#core.flush(false));
|
|
373
|
+
}, 200);
|
|
374
|
+
}
|
|
375
|
+
if (
|
|
376
|
+
effects.reconnect_after_ms !== undefined
|
|
377
|
+
&& this.#reconnectTimer === undefined
|
|
378
|
+
&& !this.#closed
|
|
379
|
+
) {
|
|
380
|
+
this.#reconnectTimer = window.setTimeout(() => {
|
|
381
|
+
this.#reconnectTimer = undefined;
|
|
382
|
+
if (this.#closed) return;
|
|
383
|
+
void this.#openSideband().catch((error) => {
|
|
384
|
+
if (!this.#closed) this.#options.onTerminated(errorMessage(error));
|
|
385
|
+
});
|
|
386
|
+
}, effects.reconnect_after_ms);
|
|
387
|
+
}
|
|
388
|
+
if (effects.terminate && !this.#closed) this.#options.onTerminated(effects.terminate);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async #openSideband() {
|
|
392
|
+
const generation = ++this.#sidebandGeneration;
|
|
393
|
+
const sidebandUrl = this.#options.sidebandUrl
|
|
394
|
+
? await this.#options.sidebandUrl(this.#sidebandCallId, this.#options.sessionId)
|
|
395
|
+
: this.#sidebandUrl;
|
|
396
|
+
if (this.#closed || generation !== this.#sidebandGeneration) return;
|
|
397
|
+
const sideband = new WebSocket(String(sidebandUrl));
|
|
398
|
+
this.#sideband = sideband;
|
|
399
|
+
let opened = false;
|
|
400
|
+
sideband.addEventListener("message", (event) => {
|
|
401
|
+
if (!this.#closed && generation === this.#sidebandGeneration) {
|
|
402
|
+
this.#applyLive(async () => {
|
|
403
|
+
await this.#admission;
|
|
404
|
+
if (this.#closed || generation !== this.#sidebandGeneration) return;
|
|
405
|
+
if (await this.#core.requiresAgentAdmission(event.data)) {
|
|
406
|
+
// Only delegations wait for durable admission. Speech deltas and
|
|
407
|
+
// agent output must continue while that independent request waits.
|
|
408
|
+
void this.#enqueue(async () => {
|
|
409
|
+
await this.#options.beforeAgentTurn?.();
|
|
410
|
+
return this.#core.realtimeMessage(event.data);
|
|
411
|
+
}, true).catch(() => {});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
return this.#core.realtimeMessage(event.data);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
sideband.addEventListener("close", () => {
|
|
419
|
+
if (!opened || this.#closed || generation !== this.#sidebandGeneration) return;
|
|
420
|
+
const connectedMs = Math.max(0, Date.now() - this.#sidebandOpenedAt);
|
|
421
|
+
this.#applyLive(() => this.#core.sidebandClosed(Math.min(connectedMs, 0xffff_ffff)));
|
|
422
|
+
});
|
|
423
|
+
await Promise.all([waitForWebSocket(sideband, this.#closing.signal), this.#admission]);
|
|
424
|
+
if (this.#closed || generation !== this.#sidebandGeneration) {
|
|
425
|
+
sideband.close();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (sideband.readyState !== WebSocket.OPEN) throw new Error("voice control connection closed during admission");
|
|
429
|
+
opened = true;
|
|
430
|
+
this.#sidebandOpenedAt = Date.now();
|
|
431
|
+
await this.#applyLive(() => this.#core.sidebandOpened());
|
|
432
|
+
if (!this.#closed && generation === this.#sidebandGeneration) {
|
|
433
|
+
this.#status(`Voice active (${this.#options.voice})`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#status(message) {
|
|
438
|
+
if (!this.#closed) this.#options.onStatus(message);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#stopBrowserIo() {
|
|
442
|
+
this.#stopBrowserMedia();
|
|
443
|
+
this.#sidebandGeneration += 1;
|
|
444
|
+
this.#sideband?.close();
|
|
445
|
+
this.#sideband = undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#stopBrowserMedia() {
|
|
449
|
+
this.#call?.abort();
|
|
450
|
+
this.#call = undefined;
|
|
451
|
+
if (this.#flushTimer !== undefined) window.clearTimeout(this.#flushTimer);
|
|
452
|
+
this.#flushTimer = undefined;
|
|
453
|
+
if (this.#reconnectTimer !== undefined) window.clearTimeout(this.#reconnectTimer);
|
|
454
|
+
this.#reconnectTimer = undefined;
|
|
455
|
+
this.#channel?.close();
|
|
456
|
+
this.#channel = undefined;
|
|
457
|
+
this.#peer?.close();
|
|
458
|
+
this.#peer = undefined;
|
|
459
|
+
stopStream(this.#microphone);
|
|
460
|
+
this.#microphone = undefined;
|
|
461
|
+
this.#speaker?.close();
|
|
462
|
+
this.#speaker = undefined;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export async function capturePreferredMicrophone(selectPhysicalInput) {
|
|
467
|
+
let microphone;
|
|
468
|
+
try {
|
|
469
|
+
microphone = await navigator.mediaDevices.getUserMedia({
|
|
470
|
+
audio: {
|
|
471
|
+
autoGainControl: true,
|
|
472
|
+
channelCount: 1,
|
|
473
|
+
echoCancellation: true,
|
|
474
|
+
noiseSuppression: true,
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
} catch (cause) {
|
|
478
|
+
throw microphoneCaptureError(cause);
|
|
479
|
+
}
|
|
480
|
+
const current = microphone.getAudioTracks()[0];
|
|
481
|
+
if (!current?.label || !navigator.mediaDevices.enumerateDevices) return microphone;
|
|
482
|
+
try {
|
|
483
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
484
|
+
const inputs = devices.filter((device) => device.kind === "audioinput" && device.label);
|
|
485
|
+
const index = await selectPhysicalInput(current.label, inputs.map((device) => device.label));
|
|
486
|
+
const physical = index === undefined ? undefined : inputs[index];
|
|
487
|
+
if (physical?.deviceId && physical.deviceId !== current.getSettings?.().deviceId) {
|
|
488
|
+
try {
|
|
489
|
+
const replacement = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: physical.deviceId } } });
|
|
490
|
+
stopStream(microphone);
|
|
491
|
+
microphone = replacement;
|
|
492
|
+
} catch {
|
|
493
|
+
// Exact-device reselection is only a desktop convenience; retain the usable capture.
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch (cause) {
|
|
497
|
+
stopStream(microphone);
|
|
498
|
+
throw cause;
|
|
499
|
+
}
|
|
500
|
+
return microphone;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function microphoneCaptureError(cause) {
|
|
504
|
+
if (cause instanceof VoiceError) return cause;
|
|
505
|
+
const name = cause && typeof cause === "object" ? cause.name : undefined;
|
|
506
|
+
if (name === "NotAllowedError" || name === "SecurityError") {
|
|
507
|
+
const policy = document.permissionsPolicy ?? document.featurePolicy;
|
|
508
|
+
const embedded = window.top !== window;
|
|
509
|
+
if (embedded && policy?.allowsFeature?.("microphone") === false) {
|
|
510
|
+
return new VoiceError(
|
|
511
|
+
"microphone_permission_blocked",
|
|
512
|
+
'Microphone access is blocked by this embed. The host iframe must allow="microphone".',
|
|
513
|
+
{ cause },
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return new VoiceError(
|
|
517
|
+
"microphone_permission_blocked",
|
|
518
|
+
"Microphone access is blocked for this site. Allow it in your browser settings, then retry.",
|
|
519
|
+
{ cause },
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (name === "NotFoundError" || name === "DevicesNotFoundError") {
|
|
523
|
+
return new VoiceError(
|
|
524
|
+
"microphone_not_found",
|
|
525
|
+
"No microphone was found. Connect a microphone, then retry.",
|
|
526
|
+
{ cause },
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (name === "NotReadableError" || name === "TrackStartError" || name === "AbortError") {
|
|
530
|
+
return new VoiceError(
|
|
531
|
+
"microphone_unavailable",
|
|
532
|
+
"The microphone is unavailable. Close other apps using it, then retry.",
|
|
533
|
+
{ cause },
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
return cause;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function acquireMicrophone(capture, signal) {
|
|
540
|
+
return new Promise((resolve, reject) => {
|
|
541
|
+
let settled = false;
|
|
542
|
+
const finish = (settle, value) => {
|
|
543
|
+
if (settled) return false;
|
|
544
|
+
settled = true;
|
|
545
|
+
window.clearTimeout(timer);
|
|
546
|
+
signal?.removeEventListener("abort", stopped);
|
|
547
|
+
settle(value);
|
|
548
|
+
return true;
|
|
549
|
+
};
|
|
550
|
+
const stopped = () => {
|
|
551
|
+
finish(
|
|
552
|
+
reject,
|
|
553
|
+
new VoiceError("microphone_capture_cancelled", "Microphone capture was stopped."),
|
|
554
|
+
);
|
|
555
|
+
};
|
|
556
|
+
const timer = window.setTimeout(() => {
|
|
557
|
+
finish(
|
|
558
|
+
reject,
|
|
559
|
+
new VoiceError(
|
|
560
|
+
"microphone_capture_timeout",
|
|
561
|
+
"The microphone did not start in time. Check your browser's selected microphone or reconnect it, then retry.",
|
|
562
|
+
),
|
|
563
|
+
);
|
|
564
|
+
}, MICROPHONE_CAPTURE_TIMEOUT_MS);
|
|
565
|
+
signal?.addEventListener("abort", stopped, { once: true });
|
|
566
|
+
if (signal?.aborted) stopped();
|
|
567
|
+
Promise.resolve(capture).then(
|
|
568
|
+
(microphone) => {
|
|
569
|
+
if (!finish(resolve, microphone)) stopStream(microphone);
|
|
570
|
+
},
|
|
571
|
+
(cause) => { finish(reject, microphoneCaptureError(cause)); },
|
|
572
|
+
);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function realtimeSidebandUrl(callId, sessionId) {
|
|
577
|
+
const url = new URL("/api/realtime/sideband", location.href);
|
|
578
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
579
|
+
url.searchParams.set("call_id", callId);
|
|
580
|
+
url.searchParams.set("session_id", sessionId);
|
|
581
|
+
return url;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function waitForWebSocket(socket, signal) {
|
|
585
|
+
if (socket.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
586
|
+
return new Promise((resolve, reject) => {
|
|
587
|
+
let timer;
|
|
588
|
+
const opened = () => { cleanup(); resolve(); };
|
|
589
|
+
const failed = () => {
|
|
590
|
+
cleanup();
|
|
591
|
+
socket.close();
|
|
592
|
+
reject(new Error("voice sideband connection failed"));
|
|
593
|
+
};
|
|
594
|
+
const closed = () => { cleanup(); reject(new Error("voice sideband closed before opening")); };
|
|
595
|
+
const stopped = () => {
|
|
596
|
+
cleanup();
|
|
597
|
+
socket.close();
|
|
598
|
+
reject(new Error("voice connection stopped"));
|
|
599
|
+
};
|
|
600
|
+
const timedOut = () => {
|
|
601
|
+
cleanup();
|
|
602
|
+
socket.close();
|
|
603
|
+
reject(new VoiceError(
|
|
604
|
+
"sideband_open_timeout",
|
|
605
|
+
"The Realtime voice sideband did not open in time. Check your network connection, then retry.",
|
|
606
|
+
));
|
|
607
|
+
};
|
|
608
|
+
const cleanup = () => {
|
|
609
|
+
window.clearTimeout(timer);
|
|
610
|
+
socket.removeEventListener("open", opened);
|
|
611
|
+
socket.removeEventListener("error", failed);
|
|
612
|
+
socket.removeEventListener("close", closed);
|
|
613
|
+
signal?.removeEventListener("abort", stopped);
|
|
614
|
+
};
|
|
615
|
+
timer = window.setTimeout(timedOut, SIDEBAND_OPEN_TIMEOUT_MS);
|
|
616
|
+
socket.addEventListener("open", opened);
|
|
617
|
+
socket.addEventListener("error", failed);
|
|
618
|
+
socket.addEventListener("close", closed);
|
|
619
|
+
signal?.addEventListener("abort", stopped, { once: true });
|
|
620
|
+
if (signal?.aborted) stopped();
|
|
621
|
+
else if (socket.readyState === WebSocket.OPEN) opened();
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function withStartupDeadline(task, { signal, timeoutMs, onTimeout, timeoutError }) {
|
|
626
|
+
return new Promise((resolve, reject) => {
|
|
627
|
+
let settled = false;
|
|
628
|
+
let timedOut = false;
|
|
629
|
+
let timer;
|
|
630
|
+
const cleanup = () => {
|
|
631
|
+
window.clearTimeout(timer);
|
|
632
|
+
signal?.removeEventListener("abort", stopped);
|
|
633
|
+
};
|
|
634
|
+
const finish = (settle, value) => {
|
|
635
|
+
if (settled) return false;
|
|
636
|
+
settled = true;
|
|
637
|
+
cleanup();
|
|
638
|
+
settle(value);
|
|
639
|
+
return true;
|
|
640
|
+
};
|
|
641
|
+
const stopped = () => {
|
|
642
|
+
if (timedOut) return;
|
|
643
|
+
finish(reject, new Error("voice connection stopped"));
|
|
644
|
+
};
|
|
645
|
+
const timeout = () => {
|
|
646
|
+
if (settled) return;
|
|
647
|
+
timedOut = true;
|
|
648
|
+
cleanup();
|
|
649
|
+
onTimeout?.();
|
|
650
|
+
settled = true;
|
|
651
|
+
reject(timeoutError);
|
|
652
|
+
};
|
|
653
|
+
timer = window.setTimeout(timeout, timeoutMs);
|
|
654
|
+
signal?.addEventListener("abort", stopped, { once: true });
|
|
655
|
+
if (signal?.aborted) {
|
|
656
|
+
stopped();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
let result;
|
|
660
|
+
try {
|
|
661
|
+
result = task();
|
|
662
|
+
} catch (cause) {
|
|
663
|
+
finish(reject, cause);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
Promise.resolve(result).then(
|
|
667
|
+
(value) => { finish(resolve, value); },
|
|
668
|
+
(cause) => { finish(reject, cause); },
|
|
669
|
+
);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function responseError(response, fallback) {
|
|
674
|
+
const body = await response.text().catch(() => "");
|
|
675
|
+
try {
|
|
676
|
+
const decoded = JSON.parse(body);
|
|
677
|
+
if (typeof decoded?.error === "string") return decoded.error;
|
|
678
|
+
} catch {}
|
|
679
|
+
return body.trim() || fallback;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function stopStream(stream) {
|
|
683
|
+
for (const track of stream?.getTracks?.() ?? []) track.stop();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function errorMessage(error) {
|
|
687
|
+
return error instanceof Error ? error.message : String(error);
|
|
688
|
+
}
|